Double OWIN authentication not working together - authentication

For my website I'm making an integration with a 3rd party authentication provider, using OWIN OpenId, to allow visitors to sign up/sign in/sign out. "Second"
I also have a test environment for my application where all code changes are tested before pushing these changes to production. I shield the test environment from public access with another 3rd party authentication provider, using OWIN OpenId as well "First". Only authenticated visitors can visit the test environment website.
Now the problem is that these both work standalone, but I can't seem to combine them. What I'm trying to achieve is that I can access the test environment by authenticating with First, and then, as a regluar visitor, authenticate with Second to see content designed for registered visitors.
Here's what I'm doing:
Both authnetication providers work with cookie authentication, but I gave them a different AuthenticationType to keep them apart.
if (IsEnabled("First"))
app.SetDefaultSignInAsAuthenticationType("First");
else
app.SetDefaultSignInAsAuthenticationType("Second");
// Configure First.
if (IsEnabled("First")) {
app.UseCookieAuthentication(First.CookieAuthenticationOptions); // AuthenticationType is set to "First" in these options.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = First.AADClientId,
Authority = First.Authority,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context => { ... },
RedirectToIdentityProvider = context => { ... }
},
AuthenticationType = "First"
});
app.Map($"{First.Path}/login", config =>
{
config.Run(context =>
{
context.Authentication.Challenge(new AuthenticationProperties
{ RedirectUri = First.ReturnUrl, IsPersistent = true },
"First"
);
context.Response.StatusCode = 401;
return context.Response.WriteAsync(string.Empty);
});
});
}
// Configure Second.
app.UseCookieAuthentication(Second.CookieAuthenticationOptions); // AuthenticationType is set to "Second" in these options.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = x => ...,
RedirectToIdentityProvider = x =>
{
var mgr = x.Options.ConfigurationManager as PolicyConfigurationManager;
if (x.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var config = await mgr.GetConfigurationByPolicyAsync(CancellationToken.None,
x.OwinContext.Authentication.AuthenticationResponseRevoke.Properties.Dictionary["PolicyId"]);
x.ProtocolMessage.IssuerAddress = config.EndSessionEndpoint;
}
else
{
var config = await mgr.GetConfigurationByPolicyAsync(CancellationToken.None,
x.OwinContext.Authentication.AuthenticationResponseChallenge.Properties.Dictionary["PolicyId"]);
x.ProtocolMessage.IssuerAddress = config.AuthorizationEndpoint;
}
var redirectUri = Second.ReturnPath;
x.ProtocolMessage.RedirectUri = redirectUri;
x.ProtocolMessage.PostLogoutRedirectUri = redirectUri;
},
SecurityTokenValidated = x => ...
},
Scope = "openid",
ResponseType = "id_token",
ReturnUri = Second.ReturnUri,
ClientId = Second.ClientId,
ConfigurationManager = GetConfigurationManager()
AuthenticationType = configuration.AuthenticationType
});
app.Map(Second.LoginPath, config =>
{
// Trigger unauthorized so that active authentication will redirect to active directory.
config.Run(context =>
{
// Set policy in context to mitigate null ref exception in Startup.Auth OnRedirectToIdentityProvider
context.Authentication.Challenge(
new AuthenticationProperties(new Dictionary<string, string>
{
{"PolicyId", Second.LoginPolicyId}
})
{
IsPersistent = true,
RedirectUri = returnUrl
}, "Second");
context.Response.StatusCode = 401;
// Middleware will redirect us instead of using this output.
return context.Response.WriteAsync(string.Empty);
});
});
app.Map(Second.ReturnPath, config =>
{
config.Use((context, next) =>
{
// In case of login, we will never get here because we will get redirected by middleware.
context.Response.Redirect("/");
return Task.FromResult(0);
});
});
When First is enabled, this allows me to do
var identity = HttpContext.Current.GetOwinContext.Authentication.AuthenticateAsync("Second").Result?.Identity;
on subsequent requests and have a ClaimsIdentity. But when First is enabled, for some reason the above Result is null.
I've noticed that when I enable both First and Second, and set the DefaultSignInAsAuthenticationType to "Second", it is First that doesn't work anymore. If I enable both First and Second, and browse the site with a previous authentication cookie of First, everything works just fine.
I'm guessing that the return method, that somewhere sets an authentication cookie needs some reference of AuthenticationType, but I don't know where to do that.
What am I missing?

The trick is to add the AuthenticationType to TokenValidationParameters when configuring Second, like so:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = x => ...,
RedirectToIdentityProvider = x =>
{
var mgr = x.Options.ConfigurationManager as PolicyConfigurationManager;
if (x.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var config = await mgr.GetConfigurationByPolicyAsync(CancellationToken.None,
x.OwinContext.Authentication.AuthenticationResponseRevoke.Properties.Dictionary["PolicyId"]);
x.ProtocolMessage.IssuerAddress = config.EndSessionEndpoint;
}
else
{
var config = await mgr.GetConfigurationByPolicyAsync(CancellationToken.None,
x.OwinContext.Authentication.AuthenticationResponseChallenge.Properties.Dictionary["PolicyId"]);
x.ProtocolMessage.IssuerAddress = config.AuthorizationEndpoint;
}
var redirectUri = Second.ReturnPath;
x.ProtocolMessage.RedirectUri = redirectUri;
x.ProtocolMessage.PostLogoutRedirectUri = redirectUri;
},
SecurityTokenValidated = x => ...
},
Scope = "openid",
ResponseType = "id_token",
ReturnUri = Second.ReturnUri,
ClientId = Second.ClientId,
ConfigurationManager = GetConfigurationManager(),
AuthenticationType = configuration.AuthenticationType,
// ADD THIS TO MAKE IT WORK:
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = configuration.AuthenticationType
}
});

Related

Asp.net core 6 mvc : Authorize controller methods with JWT token from external API

I am building a .Net core 6 mvc website which will interact with an API built by an external party. Amongst other things, the user authentication is handled by the API. The API responds with a JWT bearer token once user is authenticated and I need to tie that in to my website to Authorize controller methods.
At this point I call the API and successfully receive the token as expected, however after days of struggling to get [Authorize] to work in the controllers with the token, I am completely lost and hoping for some guidance.
After scrapping multiple iterations of code, this what I currently have.... (excuse the mess)
public async Task<TokenResponse> LoginAsync(string email, string password)
{
var userLogin = new UserLogin
{
Email = email,
Password = password
};
string encoded = System.Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(email + ":" + password));
var client = new RestClient("api location");
var request = new RestRequest();
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Authorization", "Basic " + encoded);
var response = await client.GetAsync(request);
var result = JsonConvert.DeserializeObject<TokenResponse>(response.Content);
return result;
}
public async Task<IActionResult> LoginPostAsync(LoginViewModel viewModel)
{
var tokenResponse = await _userManagementService
.LoginAsync(viewModel.Email, viewModel.Password);
if (!string.IsNullOrEmpty(tokenResponse.access_token))
{
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(tokenResponse.access_token);
var jti = jwtSecurityToken.Claims.First(claim => claim.Type == "jti").Value;
var account_type = jwtSecurityToken.Claims.First(claim => claim.Type == "account_type").Value;
var userId = jwtSecurityToken.Claims.First(claim => claim.Type == "user_id").Value;
var email = jwtSecurityToken.Claims.First(claim => claim.Type == "email").Value;
var iss = jwtSecurityToken.Claims.First(claim => claim.Type == "iss").Value;
string[] userRoles = { "admin", "candidate",};
HttpContext context = new DefaultHttpContext();
var accessToken = tokenResponse.access_token;
//var userClaims = new List<Claim>()
// {
// new Claim("email", email),
// new Claim("account_type", account_type),
// new Claim("jti", jti),
// };
//var userIdentity = new ClaimsIdentity(userClaims, "User Identity");
//var userPrincipal = new ClaimsPrincipal(new[] { userIdentity });
//context.SignInAsync(userPrincipal);
//Response.Cookies.Append(
// Constants.XAccessToken,
// tokenResponse.access_token, new CookieOptions
// {
// Expires = DateTimeOffset.UtcNow.AddMinutes(1),
// HttpOnly = true,
// SameSite = SameSiteMode.Strict
// });
//return new AuthenticateResponse(user, token);
SetJWTCookie(accessToken);
return RedirectToAction("index", "Home", new { area = "CandidateDashboard" });
}
return Unauthorized();
}
Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(config =>
{
config.SaveToken = true;
config.RequireHttpsMetadata = false;
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = false,
ValidateIssuer = true,
ValidIssuer = "issue data",
ValidateIssuerSigningKey = false,
};
config.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["Bearer"];
return Task.CompletedTask;
}
};
});
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Index()
{
return View();
}
This is the what I see in dev console.
--bearer error="invalid_token", error_description="the signature key was not found"
Payload from Bearer
{
"iss": "data here",
"exp": 1647323406,
"nbf": 1647319746,
"iat": 1647319806,
"jti": "e8f297d3-blah blah",
"account_type": "candidate",
"user_id": "2342342342",
"email": "email#email.com"
}
The core problem is that AddJwtBearer by default only trusts token issued by someone it trusts (the issuer) because it needs to verify the signature of the token. You of course want to verify it so a hacker doesn't send fake/forged tokens to your API.
So either you need to add that
.AddJwtBearer(opt =>
{
opt.Authority = "https://issuer.com"
In this way, AddJwtBearer will download the public signing key automatically for you.
Or you need to add the public signing key manually to AddJwtBearer.
see https://devblogs.microsoft.com/dotnet/jwt-validation-and-authorization-in-asp-net-core/

net core api and vue js SPA keycloak authentification

I am using net core for my back-end rest api and vuejs for the front-end.
I made an authentication via a cookie
I want to authenticate via keycloak. I want authentication to go through keycloak's authentication page and not my app's (vueJs) authentication page.
I haven't been able to find any sample code to do this.
If I do the authentication via VueJS, how then do I do so that the authentication is done on the net core API (sending the token to the API or other)?
Here is the solution.
I added this in the Startup.cs file:
private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors)
{
//It is possible inpect the certificate provided by server
Log($"Requested URI: {requestMessage.RequestUri}");
Log($"Effective date: {certificate.GetEffectiveDateString()}");
Log($"Exp date: {certificate.GetExpirationDateString()}");
Log($"Issuer: {certificate.Issuer}");
Log($"Subject: {certificate.Subject}");
//Based on the custom logic it is possible to decide whether the client considers certificate valid or not
Log($"Errors: {sslErrors}");
return true;
//return sslErrors == SslPolicyErrors.None;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = Configuration["Jwt:Authority"];
o.Audience = Configuration["Jwt:Audience"];
o.RequireHttpsMetadata = true;
o.SaveToken = true;
HttpClientHandler handler = new HttpClientHandler()
{
CheckCertificateRevocationList = false,
UseDefaultCredentials = false,
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11,
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => ServerCertificateCustomValidation(message, cert, chain, errors),
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
//ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
//ServerCertificateCustomValidationCallback = delegate { return true; },
CookieContainer = new CookieContainer()
};
//HttpClientHandler handler = new HttpClientHandler();
//handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
o.BackchannelHttpHandler = handler;
o.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username"
};
o.Events = new JwtBearerEvents()
{
OnTokenValidated = c =>
{
var test = c;
/*ClaimsIdentity identity = c.Principal.Identity as ClaimsIdentity;
identity.Se*/
JwtSecurityToken accessToken = c.SecurityToken as JwtSecurityToken;
if (accessToken != null)
{
ClaimsIdentity identity = c.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
identity.AddClaim(new Claim("access_token", accessToken.RawData));
}
}
//c.NoResult();
return Task.CompletedTask;
},
OnAuthenticationFailed = c =>
{
c.NoResult();
Log($"test");
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
//if (Environment.IsDevelopment())
//{
//return c.Response.WriteAsync(c.Exception.ToString());
//}
return c.Response.WriteAsync("An error occured processing your authentication." + c.Exception.InnerException.Message);
}
};
});
...
services.Configure<SecurityStampValidatorOptions>(options =>
{
options.ValidationInterval = TimeSpan.Zero;
});
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.DateFormatString = "yyyy-MM-ddTHH:mm:ss";
});
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp";
});
}
And in appsettings.json :
"Jwt": {
"Authority": "https://auth.myauthority/auth/realms/myauthority",
"https": null,
"Audience": "MyAudience"
},

User.Identity.Name is empty with JWT when method is no decorated with Authorize in Asp.NET Core 3.0 API Controller

I have a Web Api project in .net core 3.1 and I have added JwT authentication.
The authentication and authorization work very well, but I need to get the UserId in every request. When the method is decorated with Authorize attribute, this works well.
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IEnumerable<WeatherForecast> Get()
{
string user = User.Identity.Name; //Get a value
//Do something
}
However I have some method which authentication is not required, but if an authenticated user make a request, I would like to get the userId, but in this case, user.Identity.Name is always null.
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
string user = User.Identity.Name; //null
//Do somwthing
}
My configuration in statur file is:
private void ConfigureJwt(IServiceCollection services)
{
//Add Auth scheme
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
AuthSettings authSettings = Configuration.GetSection("AuthSettings").Get<AuthSettings>();
JwtIssuerOptions jwtIssuerOptions = Configuration.GetSection("JwtIssuerOptions").Get<JwtIssuerOptions>();
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuerOptions.Issuer,
ValidAudience = jwtIssuerOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.SecretKey))
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
//When method is no decorated with Authorize, it not working
var userId = int.Parse(context.Principal.Identity.Name);
return System.Threading.Tasks.Task.CompletedTask;
}
};
});
services.AddTransient<ITokenService, TokenService>(x =>
{
return new TokenService(Configuration);
});
}
TokenService class:
public class TokenService : ITokenService
{
IConfiguration configuration = null;
AuthSettings authSettings = null;
public TokenService(IConfiguration _configuration)
{
configuration = _configuration;
authSettings = configuration.GetSection("AuthSettings").Get<AuthSettings>();
}
public string GenerateAccessToken(IEnumerable<Claim> claims, ref JwtIssuerOptions jwtIssuerOptions)
{
//var authSettings = configuration.GetSection(nameof(AuthSettings));
//var authSettings = configuration.GetSection("EmailSettings").Get<AuthSettings>();
jwtIssuerOptions = configuration.GetSection("JwtIssuerOptions").Get<JwtIssuerOptions>();
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.SecretKey));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken (
issuer: jwtIssuerOptions.Issuer,
audience: jwtIssuerOptions.Audience,
claims: claims,
expires: jwtIssuerOptions.Expiration,
//expires: DateTime.Now.AddMinutes(5),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return tokenString;
}
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
TokenValidationParameters tokenValidationParameters = GetValidationParameters();
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
private TokenValidationParameters GetValidationParameters()
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.SecretKey)),
ValidateLifetime = false //here we are saying that we don't care about the token's expiration date
};
return tokenValidationParameters;
}
}
AuthController
[HttpPost, Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel loginModel)
{
if (loginModel == null)
return BadRequest("Invalid client request");
var sessionInfo = await userBo.LoginUser(loginModel);
if (sessionInfo == null)
return Unauthorized();
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, sessionInfo.User.BusinessEntityId.ToString()),
new Claim(ClaimTypes.Role, sessionInfo.User.RoleCode)
};
JwtIssuerOptions tokeOptions = null;
var accessToken = tokenService.GenerateAccessToken(claims, ref tokeOptions);
var refreshToken = tokenService.GenerateRefreshToken();
await tokenBo.SaveToken(
new Token()
{
BusinessEntityId = sessionInfo.Person.BusinessEntityId,
RefreshToken = refreshToken,
RefreshTokenExpiryTime = tokeOptions.Expiration
}
);
sessionInfo.TokenInfo = new TokenInfo()
{
AccessToken = accessToken,
RefreshToken = refreshToken
};
return Ok(sessionInfo);
}
}
Thank you for your help!
As far as I know, if the controller doesn't need authorize, it will not add the user information into pipeline claims, so the user name is always null.
To solve this issue, I suggest you could try to add a custom middleware to check if the request contains the Authorization header. If it contains you could get the username and add it into http context item.
Then you could directly get the username in the api controller instead of getting it from User.Identity.Name.
More details, you could refer to below codes:
Add below middleware into startup.cs Configure method:
app.Use(async (context, next) =>
{
// you could get from token or get from session.
string token = context.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(token))
{
var tok = token.Replace("Bearer ", "");
var jwttoken = new JwtSecurityTokenHandler().ReadJwtToken(tok);
var jti = jwttoken.Claims.First(claim => claim.Type == ClaimTypes.Name).Value;
context.Items.Add("Username", jti);
}
await next();
});
Controller get the username:
object value;
ControllerContext.HttpContext.Items.TryGetValue("Username", out value);
var username = value.ToString();
Result:
After changing an application from using cookie-based authentication to using JWT I ran into this problem. You can work around it — sort of — by creating an authorization handler with no requirements thus allowing anonymous users access. The ASP.NET pipeline doesn't know which requirements will be required so it will provide the credentials of the user if they are present in the request. The end result is that anonymous users are allowed but if credentials are provided they will be available.
The trivial requirement:
class RequireNothing : IAuthorizationRequirement { }
And the handler:
class RequireNothingHandler : AuthorizationHandler<RequireNothing>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireNothing requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
If the request contains credentials they will become available in the User object but the requirement also allow anonymous users access.
To use the requirement you can create a policy (and also add the handler to the DI container):
services
.AddAuthorization(options => options
.AddPolicy("AlsoAllowAnonymous", policy => policy
.AddRequirements(new RequireNothing())))
.AddSingleton<IAuthorizationHandler, RequireNothingHandler>();
To combine authenticated and anonymous access you decorate the action or controller with the attribute:
[Authorize(Policy = "AlsoAllowAnonymous")]
Unfortunately, this might not work so well. If you are using a long-lived JWT refresh tokens and short-lived access tokens that are refreshed when a 401 challenge is received there will be no challenge after the access token expires and the user will access the end-point anonymously possibly resulting in a degraded user experience even though the user has authenticated and has a refresh token to prove that.
This problem is not unique to using an authorization handler and you get more control by providing two different end-points: one for anonymous users and another one for authenticated users. You need some extra logic on the client side to select the correct API for things to work out right though.

404 error after Apple authentication using OpenId Connect

adopting from scottbrady91.com, I'm trying to have an Apple external authentication on our site. I've had Microsoft one working, but not the Apple one yet. The user is already directed to appleid.apple.com, but after authentication, it's returned to https://iluvrun.com/signin-apple (which is correct), but this isn't handled and so the user gets a 404 error.
To be honest I don't know how signin-facebook, signin-google or signin-oidc work, but they just do. So I have problems figuring out why signin-apple isn't being handled.
The site is built using ASP.NET Web Forms. Below is what I have at Startup.Auth.cs:
namespace ILR
{
public partial class Startup {
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions("Apple")
{
ClientId = "com.iluvrun.login",
Authority = "https://appleid.apple.com/auth/authorize",
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
RedirectUri = "https://iluvrun.com/signin-apple",
PostLogoutRedirectUri = "https://iluvrun.com",
Scope = "name email",
ResponseType = OpenIdConnectResponseType.Code,
ResponseMode = OpenIdConnectResponseMode.FormPost,
CallbackPath = PathString.FromUriComponent("/signin-apple"),
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://appleid.apple.com/auth/authorize",
TokenEndpoint = "https://appleid.apple.com/auth/token"
},
TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "https://appleid.apple.com",
IssuerSigningKey = new JsonWebKeySet(GetKeysAsync().Result).Keys[0]
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (context) =>
{
context.TokenEndpointRequest.ClientSecret = TokenGenerator.CreateNewToken();
return Task.CompletedTask;
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
context.Response.Redirect("/Account/Login?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
},
ProtocolValidator = new OpenIdConnectProtocolValidator
{
RequireNonce = false,
RequireStateValidation = false
}
}
);
}
private static async Task<string> GetKeysAsync()
{
string jwks = await new HttpClient().GetStringAsync("https://appleid.apple.com/auth/keys");
return jwks;
}
}
public static class TokenGenerator
{
public static string CreateNewToken()
{
const string iss = "CHM57Z5A6";
const string aud = "https://appleid.apple.com";
const string sub = "com.iluvrun.login";
const string privateKey = "XXXX"; // contents of .p8 file
CngKey cngKey = CngKey.Import(Convert.FromBase64String(privateKey), CngKeyBlobFormat.Pkcs8PrivateBlob);
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
JwtSecurityToken token = handler.CreateJwtSecurityToken(
issuer: iss,
audience: aud,
subject: new ClaimsIdentity(new List<Claim> { new Claim("sub", sub) }),
expires: DateTime.UtcNow.AddMinutes(5),
issuedAt: DateTime.UtcNow,
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(new ECDsaSecurityKey(new ECDsaCng(cngKey)), SecurityAlgorithms.EcdsaSha256));
return handler.WriteToken(token);
}
}
}
Does anyone have any clue what I miss to get this working?
You are on the right track and your question helped me to quick start my own solution for Apple ID OpenIdConnect OWIN integration in my project. After finding your post here it took me quite long to fix all issues.
After using your code I've got the same 404 error.
404 error
This was due to unhandled exception in CreateNewToken() method, which wasn't able to generate valid token. In my case it was missing Azure configuration for my AppService more described in CngKey.Import on azure:
WEBSITE_LOAD_USER_PROFILE = 1
After setting this configuration in Azure I moved to next issue:
Token endpoint wasn't called
This was due to missing configuration in OpenIdConnectAuthenticationOptions:
RedeemCode = true
This option trigger all the next processing of the authentication pipeline inside OpenIdConnect (TokenResponseReceived, SecurityTokenReceived, SecurityTokenValidated)
AccountController.ExternalLoginCallback token processing issue
This was tricky. Because all you get is after calling
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
is getting:
loginInfo == null
So after reading lot of issue articles on this topic like OWIN OpenID provider - GetExternalLoginInfo() returns null and tries, the only final workaround was to add https://github.com/Sustainsys/owin-cookie-saver to my Startup.cs, which fixed problem of missing token cookies. Its marked as legacy, but it was my only option to fix this.
So final OpenIdConnect options config for my working solution is:
var appleIdOptions = new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "https://appleid.apple.com",
ClientId = "[APPLE_CLIENT_ID_HERE]",
Authority = "https://appleid.apple.com/auth/authorize",
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
RedirectUri = "https://www.europeanart.eu/signin-apple",
PostLogoutRedirectUri = "https://www.europeanart.eu",
Scope = OpenIdConnectScope.Email,
RedeemCode = true,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
ResponseMode = OpenIdConnectResponseMode.FormPost,
CallbackPath = PathString.FromUriComponent("/signin-apple"),
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://appleid.apple.com/auth/authorize",
TokenEndpoint = "https://appleid.apple.com/auth/token"
},
TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "https://appleid.apple.com",
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKeys = new JsonWebKeySet(GetKeys()).Keys,
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (context) =>
{
var clientToken = JwtTokenGenerator.CreateNewToken();
logger.LogInfo("Apple: clientToken generated");
context.TokenEndpointRequest.ClientSecret = clientToken;
logger.LogInfo("Apple: TokenEndpointRequest ready");
return Task.FromResult(0);
},
TokenResponseReceived = (context) =>
{
logger.LogInfo("Apple: TokenResponseReceived");
return Task.FromResult(0);
},
SecurityTokenReceived = (context) =>
{
logger.LogInfo("Apple: SecurityTokenReceived");
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
string userID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
logger.LogInfo("Apple: SecurityTokenValidated with userID=" + userID);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
logger.LogInfo("Apple: RedirectToIdentityProvider");
if(context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
logger.LogInfo("Apple: RedirectToIdentityProvider -> Authenticate()");
}
else if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Token)
{
logger.LogInfo("Apple: RedirectToIdentityProvider -> Token()");
}
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
logger.LogError("Apple Authentication Failed.", context.Exception);
context.Response.Redirect("/Account/Login?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
},
ProtocolValidator = new OpenIdConnectProtocolValidator
{
RequireNonce = false,
RequireStateValidation = false
}
};

Notifications in ASP.NET Core client for IdentityServer v4

In IdentityServer 3 I used the SecurityTokenValidated event on the Notifications to build up my own identity with names and claims. For example I store the access_token to later access n API with a resource owner workflow like this:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
// ...
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
"name",
ClaimTypes.Role);
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
}
}
}
In IdentityServer 4 for ASP.NET Core is not Notifications property.
I can see that there a lot of claims automatically generated but I don't get the access_token nor the username of the identity is set automatically
My current configuration of the client in ASP.NET Core looks like this
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = identityServerUri,
RequireHttpsMetadata = false,
ClientId = clientId,
ResponseType = "id_token token",
Scope =
{
"openid profile email warehouseapi"
},
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
});
What is the intended way in IdentityServer 4 to make this ?
You can use TickedReceived event to transform claims:
var oidcOptions = new OpenIdConnectOptions
{
...
Events = new OpenIdConnectEvents()
{
// get access token
OnTicketReceived = ctx =>
{
// transform claims
var access_token = ctx.Ticket.Properties.GetTokenValue("access_token");
return Task.FromResult(0);
}
}
};
Also you don't need save tokens as claims, because when you set SaveTokens to true, tokens are automatically saved in the authentication properties. To get a token, you can use HttpContext.Authentication.GetTokenAsync("<token name>").
This is actually not related to IdentityServer4. It is rather more of a difference between the Authentication middlewares in the OWIN and AspNetCore variants.
These notifications are now more correctly named Events
you can do something similar using:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = "https://demo.identityserver.io",
PostLogoutRedirectUri = "http://localhost:3308/",
ClientId = "hybrid",
ClientSecret = "secret",
ResponseType = "code id_token",
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true,
Events = new OpenIdConnectEvents
{
OnTokenValidated = async n =>
{
}
}
});
You can find all the nice events here.