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
}
};
Related
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/
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"
},
I'm trying to get IdentityServer4 to work but unfortunately no luck. I'll explain the issue in more detail. I'm using IdentityServer4 and also .NET core Identity. I have a .net core mvc application which has login page. You basically login with username and password. When you login I need to generate jwt token I'm doing this using the following code:
[HttpGet]
public async Task<IActionResult> GetClientToken(string clientId, string clientSecret, string grantType, string scope, string username, string password)
{
var serverClient = HttpClientFactory.CreateClient();
var discoveryDocument = await serverClient.GetDiscoveryDocumentAsync($"{Request.Scheme}://{Request.Host.Value}");
var tokenClient = HttpClientFactory.CreateClient();
var tokenResponse = await tokenClient.RequestPasswordTokenAsync(
new PasswordTokenRequest
{
ClientId = clientId,
ClientSecret = clientSecret,
GrantType = grantType,
Address = discoveryDocument.TokenEndpoint,
UserName = username,
Password = password,
Scope = scope,
});
if (!tokenResponse.IsError)
{
return Ok(new TokenResponseModel()
{
access_token = tokenResponse.AccessToken,
refresh_token = tokenResponse.RefreshToken,
expires_in = tokenResponse.ExpiresIn,
scope = tokenResponse.Scope,
token_type = tokenResponse.TokenType,
});
}
return BadRequest(tokenResponse.Error);
}
Every time I request for a token I get unauthorised client.
My seeding data is as follows:
public static IEnumerable<ApiResource> GetApis() =>
new List<ApiResource>
{
new ApiResource("AppointmentBookingApi"),
new ApiResource("PaymentApi", new string[] { "patient.portal.api.payment" }),
};
public static IEnumerable<IdentityResource> GetIdentityResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResource
{
Name = "patient.portal.api",
UserClaims =
{
"patient.portal",
},
}
};
public static IEnumerable<Client> GetClients() =>
new List<Client>
{
new Client
{
ClientId = "patient.portal.client.refresh",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
RequirePkce = true,
RedirectUris = { "https://localhost:44307/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44307/Home/Index" },
AllowedScopes =
{
"AppointmentBookingApi",
"PaymentApi",
IdentityServerConstants.StandardScopes.OpenId,
"patient.portal.api",
},
AllowOfflineAccess = true,
RequireConsent = false,
},
new Client
{
ClientId = "patient.portal.client.code",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes =
{
"AppointmentBookingApi",
},
},
};
does anyone know where I'm I going wrong here????
My setup: I've created and have running a WebAPI solution that performs the authentication of a username and password against a source (currently a db). This generates the JWT token and returns it to the requesting app (a ASP.NET Core 2.2 app).
Most solutions talk of securing the WebAPI exposed methods but my approach is to only do the authentication through WebAPI. The individual apps need to accept the token so they can determine authorization.
Now the question: what is the best approach to reading the token from the WebAPI (which I've done already), validating it, and then storing it for any/all controllers to know there is an authenticated user (via Authorize attribute) so long as the token is valid?
Debugging this more, it seems my token is not being added to the headers. I see this debug message:
Authorization failed for the request at filter 'Microsoft.AspNet.Mvc.Filters.AuthorizeFilter'
Code Update2 - code that gets the JWT:
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri(_configuration.GetSection("SecurityApi:Url").Value);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//login
Task<HttpResponseMessage> response = ValidateUserAsync(client, username, password);
Task<Core.Identity.TokenViewModel> tokenResult = response.Result.Content.ReadAsAsync<Core.Identity.TokenViewModel>();
if (!response.Result.IsSuccessStatusCode)
{
if (tokenResult != null && tokenResult.Result != null)
{
ModelState.AddModelError("", tokenResult.Result.ReasonPhrase);
}
else
{
ModelState.AddModelError("", AppStrings.InvalidLoginError);
}
return View();
}
JwtSecurityToken token = new JwtSecurityToken(tokenResult.Result.Token);
int userId;
if (int.TryParse(token.Claims.First(s => s.Type == JwtRegisteredClaimNames.NameId).Value, out userId))
{
//load app claims
Core.Identity.UserInfo userInfo = Core.Identity.UserLogin.GetUser(_identityCtx, userId);
Core.Identity.UserStore uStore = new Core.Identity.UserStore(_identityCtx);
IList<Claim> claims = uStore.GetClaimsAsync(userInfo, new System.Threading.CancellationToken(false)).Result;
claims.Add(new Claim(Core.Identity.PowerFleetClaims.PowerFleetBaseClaim, Core.Identity.PowerFleetClaims.BaseUri));
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);
//complete
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.ExpiresUtc = token.ValidTo;
authProperties.AllowRefresh = false;
authProperties.IsPersistent = true;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, tokenResult.Result.Token);
//var stuff = HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, principal, authProperties);
}
else
{
ModelState.AddModelError("", AppStrings.InvalidLoginError);
return View();
}
return RedirectToAction("Index", "Home");
Startup:
private void ConfigureIdentityServices(IServiceCollection services)
{
services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");
//authentication token
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddCookie(opt =>
{
opt.LoginPath = "/Login";
opt.LogoutPath = "/Login/Logoff";
opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
ValidateLifetime = true
};
});
Core.Startup authStart = new Core.Startup(this.Configuration);
authStart.ConfigureAuthorizationServices(services);
}
Auth:
public void ConfigureAuthorizationServices(IServiceCollection services)
{
services.AddDbContext<Identity.IdentityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SecurityConn")));
services.AddScoped<DbContext, Identity.IdentityContext>(f =>
{
return f.GetService<Identity.IdentityContext>();
});
services.AddIdentityCore<Identity.UserInfo>().AddEntityFrameworkStores<Identity.IdentityContext>().AddRoles<Identity.Role>();
services.AddTransient<IUserClaimStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
services.AddTransient<IUserRoleStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
services.AddTransient<IRoleStore<Core.Identity.Role>, Core.Identity.RoleStore>();
services.AddAuthorization(auth =>
{
auth.AddPolicy(JwtBearerDefaults.AuthenticationScheme, new AuthorizationPolicyBuilder().AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build());
auth.AddPolicy(PFBaseClaim, policy => policy.RequireClaim(Identity.PFClaims.BaseUri));
});
}
In the end, my approach was to use a secure cookie and a base claim to prove the user authenticated.
private void ConfigureAuthentication(IServiceCollection services)
{
services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");
//authentication token
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/Login";
opt.AccessDeniedPath = "/Login";
opt.LogoutPath = "/Login/Logoff";
opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
ValidateLifetime = true
};
});
}
And at login:
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.ExpiresUtc = token.ValidTo;
authProperties.AllowRefresh = false;
authProperties.IsPersistent = true;
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, userStore.CreateAsync(user).Result, authProperties);
return RedirectToAction("Index", "Home");
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
}
});