I'm looking for a minimal example for a custom authentication writen in C# for asp.net core 2 based on for example API keys.
Mircosoft has a pretty good documentation about doing this with cookies, however this is not what I want. Since I want to use API keys (given by http-header, GET or Cookie, ...) I never make a call to HttpContext.SignInAsync and this is maybe the issue I can't find/google my way around.
I built an simple AuthenticationHandler (based on this - since I read that custom middlewares are not the way to go anymore) which looks something like this:
internal class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// parse cookies to find APIKEY
if(Context.Request.Cookies.ContainsKey("APIKEY"))
{
string APIKEY = Request.Cookies["APIKEY"];
// ... checking DB for APIKEY ...
// creating claims
var claims = new[]
{
new Claim( /* ... */ ),
// ...
};
var claimsIdentity = new ClaimsIdentity(claims);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
var ticket = new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties(), "Custom Scheme");
return AuthenticateResult.Success(ticket); // this line gets called
}
return AuthenticateResult.NoResult();
}
}
But when I have an API endpoint with just the [Authorize] attribute the DenyAnonymousAuthorizationRequirement denies the request cause the user is not allowed (cause IsAuthenticated == false which is readonly, all claims are shown properly)
Change var claimsIdentity = new ClaimsIdentity(claims); into something like var claimsIdentity = new ClaimsIdentity(claims, "Password"); (of course, instead of "Password" use the AuthenticationType that best fits your case).
Similar question here: Why is my ClaimsIdentity IsAuthenticated always false (for web api Authorize filter)?
Related
I am working with two identity providers, both implemented using IdentityServer4 in ASP.NET MVC Core 2.2. One of them is used as an external provider by the other. Let's call them "primary" and "external". The primary provider is referenced directly by the web application. The external provider is an optional login method provided by the primary provider.
The web application uses the oidc-client-js library to implement authentication. The logout operation in the web app calls UserManager.signoutRedirect. This works fine when the primary identity provider is used (no logout confirmation prompt is shown). However, when the external provider is used, the user is prompted to sign out from the external provider.
The sequence of requests when logging out are:
GET http://{primary}/connect/endsession?id_token_hint=...&post_logout_redirect_uri=http://{webapp}
GET http://{primary}/Account/Logout?logoutId=...
GET http://{external}/connect/endsession?state=...&post_logout_redirect_uri=http://{primary}/signout-callback-{idp}&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.3.0.0
GET http://{external}/Account/Logout?logoutId=...
This last request above shows the logout confirmation screen from the external provider.
The code for the /Account/Logout page on the primary provider is almost identical to the sample code in the documentation:
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
var vm = await BuildLogoutViewModelAsync(logoutId);
if (!vm.ShowLogoutPrompt)
{
// If the request is authenticated don't show the prompt,
// just log the user out by calling the POST handler directly.
return Logout(vm);
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated)
{
// delete local authentication cookie
await _signInManager.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
var ap = new AuthenticationProperties { RedirectUri = url };
return SignOut(ap, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
The BuildLogoutViewModelAsync method calls GetLogoutContextAsync to check if the logout is authenticated, like so:
public async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
{
var vm = new LogoutViewModel
{
LogoutId = logoutId,
ShowLogoutPrompt = true
};
var context = await _interaction.GetLogoutContextAsync(logoutId);
if (context?.ShowSignoutPrompt == false)
{
// It's safe to automatically sign-out
vm.ShowLogoutPrompt = false;
}
return vm;
}
The BuildLoggedOutViewModelAsync method basically just checks for an external identity provider and sets the TriggerExternalSignout property if one was used.
I hate to make this a wall of code, but I'll include the ConfigureServices code used to configure the primary identity server because it is probably relevant:
var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme;
opts.Authority = openIdConfig.ProviderAuthority;
opts.ClientId = openIdConfig.ClientId;
opts.ClientSecret = openIdConfig.ClientSecret;
opts.ResponseType = "code id_token";
opts.RequireHttpsMetadata = false;
opts.CallbackPath = $"/signin-{openIdConfig.Scheme}";
opts.SignedOutCallbackPath = $"/signout-callback-{openIdConfig.Scheme}";
opts.RemoteSignOutPath = $"/signout-{openIdConfig.Scheme}";
opts.Scope.Clear();
opts.Scope.Add("openid");
opts.Scope.Add("profile");
opts.Scope.Add("email");
opts.Scope.Add("phone");
opts.Scope.Add("roles");
opts.SaveTokens = true;
opts.GetClaimsFromUserInfoEndpoint = true;
var mapAdditionalClaims = new[] { JwtClaimTypes.Role, ... };
foreach (string additionalClaim in mapAdditionalClaims)
{
opts.ClaimActions.MapJsonKey(additionalClaim, additionalClaim);
}
opts.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
}
My understanding is that the id_token_hint parameter passed to the first /connect/endsession endpoint will "authenticate" the logout request, which allows us to bypass the prompt based on the ShowSignoutPrompt property returned by GetLogoutContextAsync. However, this does not happen when the user is redirected to the external provider. The call to SignOut generates the second /connect/endsession URL with a state parameter, but no id_token_hint.
The logout code in the external provider is basically the same as the code shown above. When it calls GetLogoutContextAsync, that method does not see the request as authenticated, so the ShowSignoutPrompt property is true.
Any idea how to authenticate the request to the external provider?
The final block of code, you hate, but luckily added, contains one significant row:
opts.SaveTokens = true;
That allows you later to restore the id_token you got from the external provider.Then you can use it as a "second level hint".
if (vm.TriggerExternalSignout)
{
var url = Url.Action("Logout", new { logoutId = vm.LogoutId });
var props = new AuthenticationProperties {RedirectUri = url};
props.SetParameter("id_token_hint", HttpContext.GetTokenAsync("id_token"));
return SignOut(props, vm.ExternalAuthenticationScheme);
}
I had the exact same problem as OP and was able to correct it by explicitly stating that the ID Token is to be added on to the logout request as per this Github Issue
https://github.com/IdentityServer/IdentityServer4/issues/3510
options.SaveTokens = true; // required for single sign out
options.Events = new OpenIdConnectEvents // required for single sign out
{
OnRedirectToIdentityProviderForSignOut = async (context) => context.ProtocolMessage.IdTokenHint = await context.HttpContext.GetTokenAsync("id_token")
};
I have come up with a solution, though it seems to contradict what is done in the samples.
The problem seems to be caused by two lines of code that were both from the IdentityServer samples that we used as a basis for our IDP implementations. The problem code is in the "primary" IDP.
The first line is in ConfigureServices in Startup.cs:
var authenticationBuilder = services.AddAuthentication();
authenticationBuilder.AddOpenIdConnect(openIdConfig.Scheme, "external", ConfigureOptions);
void ConfigureOptions(OpenIdConnectOptions opts)
{
opts.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
opts.SignOutScheme = IdentityServerConstants.SignoutScheme; // this is a problem
The second place is in ExternalController.cs, in the Callback method. Here we diverged from the samples, using IdentityServerConstants.ExternalCookieAuthenticationScheme instead of IdentityConstants.ExternalScheme:
// Read external identity from the temporary cookie
var result = await this.HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// ...
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme); // this is a problem
What happens at logout is: since the SignOutScheme is overridden, it is looking for a cookie that doesn't exist. Simply removing that doesn't fix it because the call to SignOutAsync has deleted the cookie that contains the information required for the identity code to authenticate the scheme. Since it can't authenticate the scheme, it does not include the id_token_hint in the request to the "external" IDP.
I've been able to fix this by removing the code that overrides SignOutScheme in Startup.cs, and moving the code that deletes the ExternalCookieAuthenticationScheme cookie to the Logout endpoint in AccountController.cs:
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// delete temporary cookie used during external authentication
await this.HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// build a return URL so the upstream provider will redirect back...
This way the "temporary" external cookie is left around until it is needed, but is deleted when the user logs out.
I'm not sure if this is the "correct" solution, but it does seem to work correctly in all cases that I've tested. I'm not really sure why we deviated from the sample in ExternalController.cs, either, but I suspect it is because we have two standalone IDP rather than a site with a single standalone IDP. Also, the sample appears to be using implicit flow while we are using hybrid flow.
In my ASP.NET MVC 5 application I need to use custom Authentication. Basically a custom library on which I call a method and which returns an object that contains information about the user.
I've created a new MVC 5 application and selected the "No Authentication" option. Then I've added an Http Module which currently looks like this:
private void Context_AuthenticateRequest(object sender, EventArgs e)
{
// Make the call to authenticate.
// This returns an object with user information.
AuthResult result = new AuthLib().SignOn();
// Inspect the returned object and create a list claims.
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, result.Username),
new Claim(ClaimTypes.GivenName, result.Name)
}
claims.AddRange(result.Groups.Select(g => new Claim(ClaimType.Role, g));
// Create principal and attach to context
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Sso");
HttpContext.Current.User = principal;
Thread.CurrentPrincipal = principal;
}
private void Context_PostAuthenticateRequest(object sender, EventArgs e)
{
var principal = ClaimsPrincipal.Current;
ClaimsAuthenticationManager transformer = FederatedAuthentication.SessionAuthenticationModule.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;
transformer.Authenticate(string.Empty, principal);
}
My claimstransformer looks like this:
public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return base.Authenticate(resourceName, incomingPrincipal);
}
ClaimsPrincipal newPrincipal = CreateApplicationPrincipal(incomingPrincipal);
EstablishSession(newPrincipal);
return newPrincipal;
}
private void EstablishSession(ClaimsPrincipal newPrincipal)
{
var sessionToken = new SessionSecurityToken(newPrincipal, TimeSpan.FromHours(8));
FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionToken);
}
private ClaimsPrincipal CreateApplicationPrincipal(ClaimsPrincipal incomingPrincipal)
{
// Convert AD group to known role in our application.
string group = incomingPrincipal.FindFirst(ClaimTypes.Role).Value;
string role = new ADGroupToRoleConverter().ConvertADGroupToRole(group);
// Add claims for group.
// These would be loaded from a db.
List<Claim> claims = new ClaimDb().GetClaimsForRole(role);
// Just copy the claims for id and given name.
claims.Add(incomingPrincipal.FindFirst(ClaimTypes.NameIdentifier));
claims.Add(incomingPrincipal.FindFirst(ClaimTypes.GivenName));
return new ClaimsPrincipal(new ClaimsIdentity(claims, "MyApp"));
}
The main issue that I'm facing is that the authentication step is called for every request even though a session exists. How can I detect that a session exists and just load the session instead of going through the entire authentication process.
Another issue is that the call to the authentication library might take a while. I guess ideally it should also be moved to the claims transformer?
Any ideas to improve this code further are also very much appreciated.
Please let me know if something is not clear or if I need to provide more detailed information.
It seems to me that you do not provide authentication information with each request after the authentication. Can you verify that you have some session cookie or authentication header sent with each request after the authentication happens?
I have a custom attribute where I manually wanna check if a claims token is valid. How do I do that?
public class AuthorizeClaimsAttribute : AuthorizeAttribute {
protected override bool UserAuthorized(IPrincipal user) {
var cookie = HttpContext.Current.Request.Cookies.Get("bearerToken");
if (cookie != null) {
//Check if token is valid, how?
}
return false;
}
}
The token is created as follow:
var identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
identity.AddClaim(new Claim("Username", model.Username));
identity.AddClaim(new Claim("IsAdmin", isAdmin.ToString()));
var properties = new AuthenticationProperties() {
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.Add(Startup.OAuthOptions.AccessTokenExpireTimeSpan)
};
var ticket = new AuthenticationTicket(identity, properties);
var accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);
Note: I cannot use the existing Authorize attribute. That´s why I need to check it manually.
One method is to store the token alongside its username somewhere in a persistant data structure
For example, when you create the Identity store the model.UserName & the accessToken in a a database of your choice.
Then, when you want to check your cookie you can re-open your database and query for it and take the appropriate action.
Also, adding that date in the database will also help you keep the size of it down resulting in faster searches, i.e. if your token only lasts for 3 months, delete the old ones as part of maintenance
in my current application I am using Owin + Aspnet Identity along with Microsoft Live OAuth provider to handle authentication.
So far everything works fine except for my attempts to retrieve the remote token, in order to store it in my database.
I have found some documentation online which says to enable "saveBootstrapContext" in the web.config, and so I did:
<system.identityModel>
<identityConfiguration saveBootstrapContext="true">
<securityTokenHandlers>
<securityTokenHandlerConfiguration saveBootstrapContext="true"></securityTokenHandlerConfiguration>
</securityTokenHandlers>
</identityConfiguration>
</system.identityModel>
I tried only on identityConfiguration then only on securityTokenHandlerConfiguration and then both together, but the result is always the same. In the following code externalData.ExternalIdentity.BootstrapContext is always null.
The SignIn method gets called inside the "ExternalLoginCallback" method which is called by the middleware.
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Web;
// custom namespaces redacted
using Microsoft.AspNet.Identity;
using Microsoft.Owin.Security;
public class AuthManager : IAuthManager
{
private readonly IUserBusinessLogic userBusinessLogic;
public AuthManager(IUserBusinessLogic userBusinessLogic)
{
this.userBusinessLogic = userBusinessLogic;
}
public void SignIn()
{
IAuthenticationManager manager = HttpContext.Current.GetOwinContext().Authentication;
var externalData = manager.GetExternalLoginInfo();
UserDto user = this.userBusinessLogic.GetUser(externalData.Login.LoginProvider, externalData.Login.ProviderKey);
var token = ((BootstrapContext)externalData.ExternalIdentity.BootstrapContext).Token;
if (user == null)
{
user = this.userBusinessLogic.AddUser(new UserDto(), externalData.Login.LoginProvider, externalData.Login.ProviderKey, token);
}
user.Token = token;
var claims = new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, user.ID.ToString()),
new Claim(ClaimTypes.UserData, UserData.FromUserDto(user).ToString())
};
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
var properties = new AuthenticationProperties
{
AllowRefresh = true,
IsPersistent = true
};
manager.SignIn(properties, identity);
}
Some other posts here on SO said to try to restart IIS, restart the machine, empty the browser cookies and restart the browser. I tried all of that and still nothing. If I mock the token string everything else works properly.
Now I am clearly missing something but I can't find any clear documentation online.
Any help is really appreciated.
Thanks.
Sometimes no help is the best help, as I've been forced to dig deeper and deeper, ultimately to find the solution.
Due premise is I was in total confusion and I was mixing three different technologies without understanding all implications.
My example used WIF configuration in web.config but then code side it was using Aspnet Identity atop of OWIN (which doesn't use web.config at all).
Once I got my ideas straight, I realized the following:
WIF was totally unneeded, therefore I got rid of all that configuration (and of WIF altogether)
Since my MS auth was being performed by the specific OWIN Middleware that handles it, I had to understand how to configure it to retrieve the token
Aspnet Identity was being used only for the DefaultAuthenticationTypes static class, which provides some string constants. I kept it for simplicity sake but I could as well remove it.
So my refactored (and working) code looks like this. First of all, the Middleware configuration needed to get MS auth working along with the token, inside Startup.cs
app.UseMicrosoftAccountAuthentication(new MicrosoftAccountAuthenticationOptions
{
ClientId = "myClientId",
ClientSecret = "myClientSecret",
Provider = new MicrosoftAccountAuthenticationProvider
{
OnAuthenticated = context =>
{
// here's the token
context.Identity.AddClaim(new System.Security.Claims.Claim("AccessToken", context.AccessToken));
context.Identity.AddClaim(new System.Security.Claims.Claim("FirstName", context.FirstName));
context.Identity.AddClaim(new System.Security.Claims.Claim("LastName", context.LastName));
return Task.FromResult(true);
}
}
});
Then the revisited SignIn method:
public void SignIn()
{
IAuthenticationManager manager = HttpContext.Current.GetOwinContext().Authentication;
var externalData = manager.GetExternalLoginInfo();
UserDto user = this.userBusinessLogic.GetUser(externalData.Login.LoginProvider, externalData.Login.ProviderKey);
if (user == null)
{
user = this.userBusinessLogic.AddUser(
new UserDto
{
FirstName = externalData.ExternalIdentity.Claims.Single(c => c.Type == "FirstName").Value,
LastName = externalData.ExternalIdentity.Claims.Single(c => c.Type == "LastName").Value
},
externalData.Login.LoginProvider,
externalData.Login.ProviderKey,
// here's the token claim that I set in the middleware configuration
externalData.ExternalIdentity.Claims.Single(c => c.Type == "AccessToken").Value);
}
var claims = new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, user.ID.ToString()),
new Claim(ClaimTypes.UserData, UserData.FromUserDto(user).ToString()),
new Claim("AccessToken", user.Token),
new Claim("FirstName", user.FirstName),
new Claim("LastName", user.LastName)
};
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
var properties = new AuthenticationProperties
{
AllowRefresh = true,
IsPersistent = true
};
manager.SignIn(properties, identity);
}
Maybe this was difficult just to me, but anyway here I am posting my solution hoping it can save some headaches and some days of swearing to some fellow developer.
Happy coding ^^
I have a legacy application from which I want to spawn a session in a new asp.net MVC application. I am attempting to pass a JSON Web Token to the MVC application to authenticate the user. I have it working for the most part, but it is requiring some extra code to get the user signed in (using Request.GetOwinContext().ctx.Authentication.SignIn) . It is making me question whether I am going down the correct path.
From what I have read I would think that I should be able to use the ClaimsPrincipal object created by the ValidateToken method to sign the user into the application.
My Issues:
The ClaimsIdentity instance created by ValidateToken sets the Authentication Type to Federated. To sign the user into the MVC application, I need to set the authentication type to Cookies (I think). To do this I am creating a new ClaimsIdentity instance based off the one created by ValidateToken and then passing ApplicationCookie as the authentication type to the constructor. Is it necessary to be doing this and is there a way to change the authentication type on the original ClaimsIdentity instance without creating a new one?
I cannot get the name property to be set automatically by the ValidateToken method. The SignIn method seems to require that to be set. To get around this I set it explicitly by pulling the name claim from the ClaimsIdentity instance created by ValidateToken (ident2.AddClaim(new Claim(ClaimTypes.Name, myIdentity.FindFirstValue("Name")));). Is there a claim I can pass in the payload of the JWT that will be mapped to this property automatically?
Or in general is this the wrong approach to accomplish the authentication?
The claims set of the payload for my JWT looks something like:
{
"iss": "http://oldapp.testing.com",
"aud": "http://newapp.testing.com",
"sub": "99239",
"iat": 1425507035,
"exp": 1425507065,
"name": "First Last",
"role": [
"Admin"
]
And the C# code to process it:
JwtSecurityToken tokenReceived = new JwtSecurityToken(token);
JwtSecurityTokenHandler recipientTokenHandler = new JwtSecurityTokenHandler();
byte[] keyBytes = Encoding.UTF8.GetBytes("someTestSecretKeyForTestingThis");
if (keyBytes.Length < 64 && tokenReceived.SignatureAlgorithm == "HS256")
{
Array.Resize(ref keyBytes, 64);
}
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidIssuer = "http://oldapp.testing.com",
ValidAudience = "http://newapp.testing.com",
IssuerSigningToken = new BinarySecretSecurityToken(keyBytes)
};
try
{
SecurityToken validatedToken;
var principal = recipientTokenHandler.ValidateToken(token, validationParameters, out validatedToken);
// Pull out the ClaimIdentity created by ValidateToken
var myIdentity = principal.Identities.FirstOrDefault();
//
// Copy ClaimIdentity created by the ValidateToken method and change the Authentication
// type from Federated to Cookie
//
// Is there a better way to do this???
//
var ident2 = new ClaimsIdentity(myIdentity.Claims, DefaultAuthenticationTypes.ApplicationCookie);
//
// Make sure the Name claim is set correctly so that the SignIn method will work
//
// Why isn't the Name claim set automatically???
//
ident2.AddClaim(new Claim(ClaimTypes.Name, myIdentity.FindFirstValue("Name")));
// Sign the user in
var ctx = Request.GetOwinContext();
var authManager = ctx.Authentication;
authManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
authManager.SignIn(ident2);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Exception :" + ex.Message);