Iam working on app, which consists from angular frontend and ASP net Web API backend(.net 4.5). For authentication iam using OpenIdConnect. I succesfully connected frontend to identity provider but now i need to validate id token on backend, so i can be sure, that only validated users can call backend.
This id token use rs256 algorithm for signing. So on backend, i need to do two things:
Get JWKs from identity provider URL - iam a little lost here, should i get it throug normal HttpClient, or there is some library or helper function to do this?
Generate RSA public key out of JWKs and validate token - for this iam using this function:
string token="xyz..";
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters()
{
Modulus = FromBase64Url("xyz.."),
Exponent = FromBase64Url("xyz..")
});
var validationParameters = new TokenValidationParameters
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = true,
IssuerSigningKey = new RsaSecurityKey(rsa)
};
SecurityToken validatedSecurityToken = null;
var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(tokenStr, validationParameters, out validatedSecurityToken);
JwtSecurityToken validatedJwt = validatedSecurityToken as JwtSecurityToken;
It works, but now i need to connect it somehow with the loaded JWKs and register it to use this for every request that comes. Any advices or simple example would really help me. Thx.
This code below is taken from one of my training classes and it will automatically download and validate the provided token and I hope you can use it as a reference. You typically will use the ConfigurationManager to download the IdentityServer configuration and JWKS for you. It will also internally cache and periodically (every 24) readload the config.
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using OpenID_Connect_client.Models;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading;
namespace OpenID_Connect_client
{
public class TokenValidator
{
private readonly IOpenIDSettings openIDSettings;
public TokenValidator(IOpenIDSettings openIDSettings)
{
this.openIDSettings = openIDSettings;
}
public string ValidateToken(string token, string clientId)
{
try
{
string issuer = openIDSettings.Issuer;
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{issuer}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
var openIdConfig = configurationManager.GetConfigurationAsync(CancellationToken.None).Result;
// Configure the TokenValidationParameters. Assign the SigningKeys which were downloaded from Auth0.
// Also set the Issuer and Audience(s) to validate
//https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs
var validationParameters =
new TokenValidationParameters
{
IssuerSigningKeys = openIdConfig.SigningKeys,
ValidAudiences = new[] { clientId },
ValidIssuer = issuer,
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateTokenReplay = true
};
// Now validate the token. If the token is not valid for any reason, an exception will be thrown by the method
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var user = handler.ValidateToken(token, validationParameters, out validatedToken);
// The ValidateToken method above will return a ClaimsPrincipal. Get the user ID from the NameIdentifier claim
// (The sub claim from the JWT will be translated to the NameIdentifier claim)
return $"Token is validated. User Id {user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value}";
}
catch (Exception exc)
{
return "Invalid token: " + exc.Message;
}
}
}
}
Related
I need to port over an application from ASP .NET WebApi to ASP .NET Core. The existing application uses an AuthorizationFilterAttribute to check for a Bearer JWT in the header and validates it manually by fetching the crypto keys from the issuer and checking some roles. I will post the existing code below.
As AuthorizationFilter-Attributes are not a thing anymore in ASP .NET Core, I am looking for the correct way to do this now.
The samples and documentation I found are all about issuing your own tokens, which I do not want. My attempt to write my own policy to check the token failed because apparently there needs to be authentication done before you can use policies. The existing application does not do any authentication, it's simply a matter of fetching the JWT and validating it against the issuer as can be seen in the code below.
Any help on how to guard a controller action by checking a supplied (issued by another system) OpenID-Connect token would be greatly appreciated!
Existing code for validating the supplied JWT:
JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtSecurityToken = jwtSecurityTokenHandler.ReadJwtToken(token);
string openIdConnectDiscoveryUrl = jwtSecurityToken.Issuer.Trim();
if (!openIdConnectDiscoveryUrl.EndsWith("/", StringComparison.Ordinal))
{
openIdConnectDiscoveryUrl += "/";
}
openIdConnectDiscoveryUrl += ".well-known/openid-configuration";
string n;
string e;
using (HttpClient httpClient = new HttpClient())
{
string openidConfiguration = httpClient.GetStringAsync(new Uri(openIdConnectDiscoveryUrl, UriKind.Absolute)).Result;
dynamic openidConfigurationJson = JObject.Parse(openidConfiguration);
string jwksUri = openidConfigurationJson.jwks_uri;
string certs = httpClient.GetStringAsync(new Uri(jwksUri, UriKind.Absolute)).Result;
dynamic certsJson = JObject.Parse(certs);
dynamic key = certsJson.keys[0];
n = key.n;
e = key.e;
}
using (RSACryptoServiceProvider rsaCryptoServiceProvider = new RSACryptoServiceProvider())
{
rsaCryptoServiceProvider.ImportParameters(new RSAParameters { Modulus = FromBase64Url(n), Exponent = FromBase64Url(e) });
TokenValidationParameters validationParameters = new TokenValidaAtionParameters
{
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidateActor = false,
ValidateTokenReplay = false,
RequireSignedTokens = true,
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKey = new RsaSecurityKey(rsaCryptoServiceProvider)
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
handler.ValidateToken(token, validationParameters, out SecurityToken validatedSecurityToken);
jwtSecurityToken = validatedSecurityToken as JwtSecurityToken;
// removed some code that checks the existence of specific roles in the token
I'm trying to understand the code block below. Everything is fine until the third argument of the tokenHandler.ValidateToken(...) method which is out SecurityToken validatedToken.
I checked the docs but could not find out much about that last argument. How does it work?
And what happens to tokenHandler.ValidateToken(...)? we are not assigning its return value to anything it looks like it just stays there and idk.
Could you make these clear for me?
private void attachUserToContext(HttpContext context, IUserService userService, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
// attach user to context on successful jwt validation
context.Items["User"] = userService.GetById(userId);
}
catch
{
// do nothing if jwt validation fails
// user is not attached to context so request won't have access to secure routes
}
The method ValidateToken() takes the received token as a String, validates the token according to the TokenValidationParameters and creates an object of type SecurityToken, which is returned via the out parameter.
In the next line this object is casted to the type JwtSecurityToken
var jwtToken = (JwtSecurityToken)validatedToken;
and then parsed
var userId = int.Parse(...)
to get the userId and finally the HttpContext context
get's populated
context.Items["User"] = userService.GetById(userId);
with the user information. If validation is successful, you have the user information available via the httpContext.
I have an asp.net 4.6 web forms application (no MVC). I am updating the security in my application. I am using OpenIdConnectAuthentication to authenticate with our Azure AD. Then I pass the access token to Microsoft graph to send an email with Office 365. My token is set to expire in 60 minutes. I either need to expand the expiration to 8 hours or refresh the token. Without having MVC I am not sure how to handle this. I am looking for help with direction to take and possibly code samples.
(I original tried to utilize an MVC sample and put it into my project using a Session Token class. Once we tested with multiple users I believe I had a memory leak and it would crash in about 5 minutes.)
Startup code:
public class Startup
{
private readonly string _clientId = ConfigurationManager.AppSettings["ClientId"];
private readonly string _redirectUri = ConfigurationManager.AppSettings["RedirectUri"];
private readonly string _authority = ConfigurationManager.AppSettings["Authority"];
private readonly string _clientSecret = ConfigurationManager.AppSettings["ClientSecret"];
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebCookieManager(),
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = _clientId,
ClientSecret = _clientSecret,
//Authority = _authority,
Authority = String.Format(_authority, domain, "/v2.0"),
RedirectUri = _redirectUri,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = OpenIdConnectScope.OpenIdProfile,
UseTokenLifetime = false,
TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RequireExpirationTime = false},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// Exchange code for access and ID tokens
var auth = String.Format(_authority, "common/oauth2/v2.0", "/token");
var tokenClient = new TokenClient($"{auth}", _clientId, _clientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, _redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
var claims = new List<Claim>()
{
new Claim("id_token", tokenResponse.IdentityToken),
new Claim("access_token", tokenResponse.AccessToken)
};
n.AuthenticationTicket.Identity.AddClaims(claims);
},
},
});
}
}
SDK Helper:
public class SDKHelper
{
// Get an authenticated Microsoft Graph Service client.
public static GraphServiceClient GetAuthenticatedClient()
{
GraphServiceClient graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = System.Security.Claims.ClaimsPrincipal.Current.FindFirst("access_token").Value;
// Append the access token to the request.
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
// Get event times in the current time zone.
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
// This header has been added to identify our sample in the Microsoft Graph service. If extracting this code for your project please remove.
requestMessage.Headers.Add("SampleID", "aspnet-snippets-sample");
}));
return graphClient;
}
}
Sending Email:
GraphServiceClient graphClient = SDKHelper.GetAuthenticatedClient();
string address = emailaddress;
string guid = Guid.NewGuid().ToString();
List<Recipient> recipients = new List<Recipient>();
recipients.Add(new Recipient
{
EmailAddress = new Microsoft.Graph.EmailAddress
{
Address = address
}
});
// Create the message.
Message email = new Message
{
Body = new ItemBody
{
ContentType = Microsoft.Graph.BodyType.Text,
},
Subject = "TEST",
ToRecipients = recipients,
From = new Recipient
{
EmailAddress = new Microsoft.Graph.EmailAddress
{
Address = address
}
}
};
// Send the message.
try
{
graphClient.Me.SendMail(email, true).Request().PostAsync().Wait();
}
catch (ServiceException exMsg)
{
}
You need to request the scope offline_access. Once you've requested that, the /token endpoint will return both an access_token and a refresh_token. When your token expires, you can make another call to the /token endpoint to request a new set of access and refresh tokens.
You might find this article helpful: Microsoft v2 Endpoint Primer. In particular, the section on refresh tokens.
I have tried implementing ASOS with .net core 2.1 and there were few things which were available in OAuthAuthorizationProvider but I couldn't find them in ASOS. Also I think the context is little different in ASOS, So is there any alternate of the following code in ASOS:
OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
var options = new OAuthAuthorizationServerOptions
{
AuthorizeEndpointPath = new PathString(AuthorizePath),
TokenEndpointPath = new PathString(TokenPath),
ApplicationCanDisplayErrors = true,
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5),
#if DEBUG
AllowInsecureHttp = true,
#endif
// Authorization server provider which controls the lifecycle of Authorization Server
Provider = new OAuthAuthorizationServerProvider
{
OnValidateClientRedirectUri = ValidateClientRedirectUri,
OnValidateClientAuthentication = ValidateClientAuthentication,
OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials,
OnGrantClientCredentials = GrantClientCredetails
},
// Authorization code provider which creates and receives authorization code
AuthorizationCodeProvider = new AuthenticationTokenProvider
{
OnCreate = CreateAuthenticationCode,
OnReceive = ReceiveAuthenticationCode,
},
// Refresh token provider which creates and receives referesh token
RefreshTokenProvider = new AuthenticationTokenProvider
{
OnCreate = CreateRefreshToken,
OnReceive = ReceiveRefreshToken,
}
,
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(OAuthBearerOptions);
Update:
private Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("claim", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("claim", x)));
context.Validated(identity);
return Task.FromResult(0);
}
Most of the options are still there but the events model has been reworked:
OnValidateClientRedirectUri was replaced by a more general OnValidateAuthorizationRequest event.
OnValidateClientAuthentication no longer exists. Client authentication validation is now performed in the OnValidateTokenRequest event (or OnValidateIntrospectionRequest/OnValidateRevocationRequest, but you're not using the introspection/revocation endpoints in your snippet).
The *Provider properties - used for decrypting/encrypting tokens - have been replaced by Serialize* and Deserialize* events. Using them is no longer mandatory: in this case, authorization codes and refresh tokens will be considered valid until they expire.
If you want to learn more about the revamped events model, don't miss this blog post series: https://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-introduction/
Following this guide I was able to get authentication working using
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.AspNetCore.Authentication.JwtBearer
Now I'm trying to secure my api endpoints with either roles, or claims. I've tried both with the same result (403)
Using just [Authorize] works fine.
My code currently looks something like this:
Controller:
[Authorize(Policy = "RequireUserRole")]
// Also tried [Authorize(Roles="User")]
public string Get()
{
return "YO";
}
Startup:
services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<ApplicationContext>();
services.Configure<JWTSettings>(Configuration.GetSection("JWTSettings"));
services.AddAuthorization(options =>
{
options.AddPolicy("RequireUserRole", policy => policy.RequireRole("User"));
});
...
app.UseIdentity();
var secretKey = Configuration.GetSection("JWTSettings:SecretKey").Value;
var issuer = Configuration.GetSection("JWTSettings:Issuer").Value;
var audience = Configuration.GetSection("JWTSettings:Audience").Value;
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = issuer,
// Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = audience,
ValidateLifetime = true
}
});
app.UseMvcWithDefaultRoute();
and when I create the user I assign it to the role "User"
await _userManager.AddToRoleAsync(user, "User");
The role relationship is being created successfully, but the validation for the role when hitting the endpoint is failing.
Any help appreciated!
The answer is in this mdsn blog post:
Authorizing based on roles is available out-of-the-box with ASP.NET
Identity. As long as the bearer token used for authentication contains
a roles element, ASP.NET Core’s JWT bearer authentication middleware
will use that data to populate roles for the user.
So, a roles-based authorization attribute (like [Authorize(Roles =
"Manager,Administrator")] to limit access to managers and admins) can
be added to APIs and work immediately.
So I added an element to my access token object called roles:
private string GetAccessToken(string userRole)
{
var payload = new Dictionary<string, object>
{
...
{ "roles", userRole }
};
return GetToken(payload);
}
I read that msdn post and added in startup:
internal class HasRoleRequerementAuthHandler : AuthorizationHandler<HasRoleRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasRoleRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == JwtRegisteredClaimNames.NameId))
if (context.User.FindFirst(c => c.Type == JwtRegisteredClaimNames.Typ).Value == requirement.RoleName)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
internal class HasRoleRequirement : IAuthorizationRequirement
{
public readonly string RoleName;
public HasRoleRequirement(string roleName)
{
RoleName = roleName;
}
}
Then added policy:
opt.AddPolicy("Test", builder =>
{
builder.Requirements.Add(new HasRoleRequirement("User"));
});
My token is generated like this:
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.NameId, user.Uid),
new Claim(JwtRegisteredClaimNames.Typ, type.ToString())
};
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature);
var tokenDesc = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(7),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDesc);
return tokenHandler.WriteToken(token);
And then I can use policy Auth header:
[HttpGet("TestAuth")]
[Authorize(Policy = "Test")]
public IActionResult TestAuth()
{
return new OkResult();
}