I'm trying to validate a token that was provided by Azure AD inside of an AWS Lambda Function. At the moment I have a MVC Website that you can authenticate to Azure AD with, which returns a JWT Token. This JWT will be passed up to an AWS API Gateway where a Lambda Authorizer will verify it.
At first I thought the correct method was to pass the JWT back to Azure AD to verify the token. However after reading this, it appears I need to decrypt the token, and validate the issuer and audience. This lead me to this, which does successfully validate the token. However, if i change mySecret to not match the one configured in Azure AD, it still successfully validates?
var authToken = "JWTToken";
string key = "I thought this needed to be the client secret in Azure AD but any string will still pass verification";
string myTenant = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
var myAudience = "api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
var myIssuer = string.Format(CultureInfo.InvariantCulture, "https://sts.windows.net/{0}/", myTenant);
var mySecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key));
var stsDiscoveryEndpoint = String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", myTenant);
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
var config = await configManager.GetConfigurationAsync();
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = myAudience,
ValidIssuer = myIssuer,
IssuerSigningKeys = config.SigningKeys,
ValidateLifetime = false,
IssuerSigningKey = mySecurityKey,
ValidateAudience = true,
ValidateIssuer = true,
};
var validatedToken = (SecurityToken)new JwtSecurityToken();
// Throws an Exception as the token is invalid (expired, invalid-formatted, etc.)
tokenHandler.ValidateToken(authToken, validationParameters, out validatedToken);
I have Azure AD configured to this, where the Client is the MVC website and the Service is the Lambda Auhorizer. TLDR: This is basically two client registrations, where the Lambda Authoizer has an exposed API and the MVC website has a client secret.
Have a taken the right approach? If so, how do I fix the issue i am facing?
Any help would be appreciated
I've used something like this:
string authority = "https://login.microsoftonline.com/<your-tenant-id>/";
string clientId = "<your-client-id>";
IConfigurationManager<OpenIdConnectConfiguration> configurationManager =
new ConfigurationManager<OpenIdConnectConfiguration>($"{authority}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
var validationParams = new TokenValidationParameters
{
ValidAudience = clientId,
IssuerSigningKeys = openIdConfig.SigningKeys
};
It uses the authority to download the valid issuer, signing keys etc.
Related
I am trying to validate a valid JWT using this code below but am getting a strange error
[Error] IDX10516: Signature validation failed. Unable to match key:
kid: '-KI3Q9nNR7bRofxmeZoXqbHZGew'.
Number of keys in TokenValidationParameters: '1'.
Number of keys in Configuration: '0'.
Exceptions caught:
'[PII of type 'System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
token: '[PII of type 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. Valid Lifetime: 'True'. Valid Issuer: 'False'
Code:
I had seen a lot of reference links on this issue but still not getting what am I missing?
I tried to test locally, its worked perfectly but after deployed to Azure Function its failing.
Configuration Settings:
"JwtSettings:TenantName": "OSHDev.onmicrosoft.com",
"JwtSettings:TenantId": "5492b240-96ee-44a1-bdcb-fa0ba0200111",
"JwtSettings:AadB2cInstance": "https://OSHDev.b2clogin.com/{0}/v2.0/",
"JwtSettings:OpeinConfigUrl":"https://OSHDev.b2clogin.com/OSHDev.onmicrosoft.com/B2C_1A_SIGNUP_SIGNIN/v2.0/.well-known/openid-configuration",
public async Task<ClaimsPrincipal> ValidateAccessToken(string accessToken, JwtSettings jwtSettings, ILogger logger)
{
var audience = jwtSettings.Audience;
var tenant = jwtSettings.TenantName;
var tenantid = jwtSettings.TenantId;
var aadb2cInstance = jwtSettings.AadB2cInstance;
var openidconfigurl = jwtSettings.OpeinConfigUrl;
//Debugging purposes only, set this to false for production
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = false;
ConfigurationManager<OpenIdConnectConfiguration> configManager =
new ConfigurationManager<OpenIdConnectConfiguration>(
openidconfigurl,
new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config;
config = await configManager.GetConfigurationAsync();
//Microsoft Identity to override claim names . If we remove below code line, "sub" claim will not be visible. Its visible under "nameidentifier"
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler? tokenValidator = new JwtSecurityTokenHandler();
// Initialize the token validation parameters
TokenValidationParameters validationParameters = new TokenValidationParameters
{
// App Id URI and AppId of this service application are both valid audiences.
ValidateAudience = true,
ValidAudiences = new[] { audience },
ValidateIssuer= true,
ValidIssuers = new List<string>()
{
string.Format(CultureInfo.InvariantCulture, aadb2cInstance, tenantid),
string.Format(CultureInfo.InvariantCulture, aadb2cInstance, tenant)
},
ValidateIssuerSigningKey = true,
// Support Azure AD V1 and V2 endpoints.
IssuerSigningKeys = config.SigningKeys,
RequireSignedTokens = true,
//Debugging purposes only, set this to true for production
ValidateLifetime = true
};
try
{
//Validate JwTToken and return Claims Prinicpals
ClaimsPrincipal? claimsPrincipal = tokenValidator.ValidateToken(accessToken, validationParameters, out SecurityToken securityToken);
return claimsPrincipal;
}
catch (Exception ex)
{
logger.LogError(ex.Message);
}
return null;
}
Like the error said, your token validator try to find the public key that is used to sign your token in the jwks_uri, you can find it in your well-known URL.
In each token there is a header "KID", this header indicate the ID of the public key used to sign your token.
When i check your jwks_uri there is no KID with ID :
"-KI3Q9nNR7bRofxmeZoXqbHZGew"
But:
"veMP2TrHLgs4XvKYJhumvhPW6O-WpbdSGqKdetRmvxI"
I can't read token claims from Bearer JWT token.
Login is working, the HTTP request comes with a valid JWT token to the backend.
Login returns the token.
Here is my code on server side:
Program.cs
builder.Services.AddAuthentication(m => {
//m.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
m.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
m.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(conf => {
conf.RequireHttpsMetadata = false;
conf.SaveToken = true;
conf.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = Configuration["JWT-Issuer"],
ValidAudience = Configuration["JWT-Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT-Key"])),
ClockSkew = TimeSpan.Zero,
};
});
When I uncomment this line //m.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; postman says unauthorized. When it is kept commented, authorization succeeds.
Token is generated here.
GenerateToken Method:
private object GenerateJwtToken(string Id, string email, ApplicationUser appUser, string appUserRole, string FirstName)
{
List<Claim> claims = null;
claims = new List<Claim> {
new Claim(JwtRegisteredClaimNames.Email,email),
new Claim(JwtRegisteredClaimNames.Jti, appUser.Id),
new Claim("Role",appUserRole),
new Claim("UserName",appUser.UserName),
new Claim("TEMP", FirstName)
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT-Key"]));
var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expire = DateTime.Now.AddDays(Convert.ToDouble(_configuration["JWT-Expiry"]));
var token = new JwtSecurityToken(
issuer: _configuration["JWT-Issuer"],
audience: _configuration["JWT-Issuer"],
claims: claims,
expires: expire,
signingCredentials: cred
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
When JWT Bearer token is passed to the API call decorated with [Authorize] and tested with debugger it shows that custom claim like TEMP is not present in the User.Claims List.
Reading Claims From Token
string Email = User.Claims.SingleOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")).Value;
string FirstName= User.Claims.SingleOrDefault(x => x.Type.Equals("TEMP")).Value;
Here, the email can be read successfully but I can't read the FirstName from the token. In fact User.Claims doesn't have the FirstName claim (I mean all the custom claims leaving the Registered JWT Default ClaimTypes), it only has the default token parameters which are emailaddress, id, role etc.
What should I do?
should I need to create custom authentication scheme for this purpose?
Internally in AddJwtBearer, there are some remapping done by the Token hander, where claims are renamed, like
email -> http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
This mapping can be turned of using:
// Or set this flag to false
.AddJwtBearer(opt =>
{
...
opt.MapInboundClaims = false;
});
or setting:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
The actual mapping can be found here
However, I am not sure why the TEMP claim is not found. What claims do you actually see in the authenticated User in your controller? Some claims in some cases are also ignored.
I am making custom Single-Sign-On service with asp.net core. I made login server which returns JWT token in the cookie with validation. I would like to connect my web application to the login server, so the web application recognize if the user is logged in or not by login server.
Is it possible to do it with startup.cs setting in the web application? And if so how can I make it? I don't want to put the token checking process in every actions.
Thanks in advance
As far as I know, in asp.net core we could provide the jwtbear authentication validation to check if the user login or not. All these things is provided at the web application not login server. If you send the token to the login server how the web application know the user is login or not.
Normally, we will directly check the user token is valid or not on the web application side.
We could set the login server and web application use the same Issuer, Audience and SecretKey.
Then we you could directly validate the user in the web application instead of login server.
More details, you could refer to below codes:
Login server generate token:
public static string GetToken3()
{
var key = Encoding.ASCII.GetBytes
("YourKey-2374-OFFKDI940NG7:56753253-tyuw-5769-0921-kfirox29zoxv");
//Generate Token for user
var JWToken = new JwtSecurityToken(
issuer: "http://localhost:45092/",
audience: "http://localhost:45092/",
claims: new List<Claim>
{
new Claim(ClaimTypes.Name, "Users2222" )
},
notBefore: new DateTimeOffset(DateTime.Now).DateTime,
expires: new DateTimeOffset(DateTime.Now.AddDays(1)).DateTime,
//Using HS256 Algorithm to encrypt Token
signingCredentials: new SigningCredentials
(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
);
string token = new JwtSecurityTokenHandler().WriteToken(JWToken);
return token;
}
web application validate token:
//Provide a secret key to Encrypt and Decrypt the Token
var SecretKey = Encoding.ASCII.GetBytes
("YourKey-2374-OFFKDI940NG7:56753253-tyuw-5769-0921-kfirox29zoxv");
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "http://localhost:45092/",
ValidAudience = "http://localhost:45092/",
IssuerSigningKey = new SymmetricSecurityKey(
SecretKey),
ClockSkew = TimeSpan.Zero
}
);
we are using Azure Active Directory for our company. We have an internal web project with Asp.Net Core 3.
We are using a React as a frontend. The user is able to login with his AAD Credentials and the React app gets a token. The Token should be used to access functions in the ASP.Net Core project. Therefore the token should be validated in ASP.Net Core. Here is my problem. I was able to call the token validation but I'm getting different errors.
The ASP.Net app: https://localhost:44350/
The react app: https://localhost:44395/
This is one of the ASP actions:
[HttpGet]
[Route("GetArticles")]
public async Task<JsonResult> GetArticles()
{
if (!Request.Headers.TryGetValue("Authorization", out var authorizationToken))
{
return Json(Unauthorized());
}
if (!authorizationToken.Any())
{
return Json(Unauthorized());
}
var jwt = await Validate(authorizationToken.First());
return Json(await _unitOfWork.ArticlesRepos.GetAllAsync());
}
The token is passed and the validation is triggered. The validation is in the Validate method:
public async Task<JwtSecurityToken> Validate(string token)
{
string tenant = _configuration.GetValue<string>("Tenant");
string publicKey = _configuration.GetValue<string>("AadPubKey");
//string stsDiscoveryEndpoint = $"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
//string stsDiscoveryEndpoint = $"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token";
string stsDiscoveryEndpoint = $"https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration";
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();
var IssuerSigningKeys = config.SigningKeys;
var x = new X509Certificate2(Encoding.ASCII.GetBytes(publicKey));
var y = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(publicKey));
var rsa = new RSACryptoServiceProvider();
string exponentvalue = "AQAB";
var e = Base64UrlEncoder.DecodeBytes(exponentvalue);
var N = publicKey;
var modulus = Base64UrlEncoder.DecodeBytes(N);
rsa.ImportParameters(
new RSAParameters()
{
Modulus = modulus,
Exponent = e
});
var signingKey = new RsaSecurityKey(rsa);
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = true,
ValidIssuer = stsDiscoveryEndpoint,
//ValidIssuer = "https://localhost:44350/",
//ValidAudience = "https://localhost:44395/",
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateLifetime = false
};
JwtSecurityTokenHandler tokendHandler = new JwtSecurityTokenHandler();
var result = tokendHandler.ValidateToken(token, validationParameters, out SecurityToken jwt);
return jwt as JwtSecurityToken;
}
As you can see, we tried diffferent things. We are getting following errors at the line with
var result = tokendHandler.ValidateToken(token, validationParameters, out SecurityToken jwt);:
When we use IssuerSigningKey = y, =>
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException
HResult=0x80131500
Message = IDX10501: Signature validation failed. Unable to match key:
kid: 'someId'.
Exceptions caught:
'System.NotSupportedException: IDX10634: Unable to create the SignatureProvider.
Algorithm: 'RS256', SecurityKey: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '', InternalId: ''.'
is not supported
When we use IssuerSigningKey = signingKey, =>
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException
HResult=0x80131500
Message = IDX10501: Signature validation failed. Unable to match key:
kid: 'someId'.
Exceptions caught:
''.
I have no idea how to configure the TokenValidationParameters. When I look in https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys I see the key which is stated in the Exception. But there are numerous informations.
{"keys":
[{"kty":"RSA"
,"use":"sig"
,"kid":"someId"
,"x5t":"someId"
,"n":"longString"
,"e":"AQAB"
,"x5c":["otherLongString"]
,"issuer":"https://login.microsoftonline.com/myTenant/v2.0"},
{...},{...}]
How can we make the validation work?
Thanks in advance
Getting an IDX10634 error from an RS256 algorithm using a SymmetricSecurityKey looks like behavior that has been fixed in newer versions of IdentityModel. Which version of the IdentityModel assemblies are you using? Please try updating to the latest version (currently 6.7.1) and see if your issue persists.
As a few side notes :
JwtSecurityTokenHandler / TokenValidationParameters are meant to be long lived objects. TokenValidationParameters for example, has a cache that can be leveraged when reused.
Is this a SPA app? If it is, this sample should be useful:
https://github.com/Azure-Samples/ms-identity-javascript-react-spa-dotnetcore-webapi-obo
We have a security requirement that we must validate the id token we receive from Azure AD B2C. We need to validate these at the minimum
customSecurityLevel, audience, not before and "expiration time", issuer, nonce
Looking asp.net MVC OWIN middleware, I noticed that that OpenIdConnectAuthenicationOptions provides these:
return new OpenIdConnectAuthenticationOptions
{
...
Notifications = new OpenIdConnectAuthenticationNotifications //Specifies events which the OpenIdConnectAuthenticationMiddleware invokes to enable developer control over the authentication process.
{
AuthenticationFailed = this.AuthenticationFailed,
RedirectToIdentityProvider = this.RedirectToIdentityProvider,
AuthorizationCodeReceived = this.OnAuthorizationCodeReceived,
},
TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true, // Important to save the token in boostrapcontext
ValidateAudience = true, // Validate the Audience
ValidateIssuer = true, // Validate the Issuer
ValidateLifetime = true, // Validate the tokens lifetime
ValidIssuer = Issuer, // The issuer to be validated
ValidAudience = ClientId, // The Audience to be validated
},
};
Being quiet new to OWIN, I'm trying to understand below:
Does OWIN middleware magically validates the token we receive from Azure AD B2C or do we need to manually perform validation per this:
https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-webapi-manual-jwt-validation/
At what point in time should token validation should occur i.e. AuthorizationCodeReceived event or on redirect controller/action (page) that's configured on Azure AD B2C redirect URL?
We need to validate more attributes that TokenValidationParameters supports e.g. customSecurityAttribute we send on initial payload. Is there a way to extend this?
How do we parse the token that we receive from Azure AD B2C using OWIN?
Any code sample would be handy.
TO make your question simpler.
The idea behind token is to parse the token and get 3 parts from the token
-Header : contain information about in which algorithm the token haven been encrypted
-Payload : information about the user
-Signature: it's the calculation of encryption of ( Header + Payload) using the Azure certificate or( your identity provider).
Next step the user sends request to your back-end along with JWT.
your back-end will parse the token and get certificate type then will preform HTTP request to your identity provider to get certificate
Next your back-end will construct the certificate option and try to do encryption for ( header + Payload) came from your token the output string must be exactly same Signature you received in the token from your front-end.
If every thing is okay
now your back-end will start validating other attributes like Audience, Issuer
if you configure your token to validate the Audience means your front-end required to provide token contain Audience(Application ID) exactly same your back-end as well as for issuer.
the question now how my back-end knows about certificate?
Azure AD using OpenID connect, More information here
since you configred you tenant in backend the auth packge will make a call to https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration to get the details about your identity provider
and one important link as will is ("jwks_uri": "https://login.microsoftonline.com/common/discovery/keys") where the signature hosted.
You can read and search more how to validate certificate JWT and check this
https://codereview.stackexchange.com/questions/70005/authentication-with-jwt
moving to part 2 of validate more attributes.
Since you are using OpenIdConnect, the package has class called OpenIdConnectEvents that you can trigger events and do what ever you want like this
.AddOpenIdConnect(o =>
{
//Additional config snipped
o.Events = new OpenIdConnectEvents
{
OnTokenValidated = async ctx =>
{
//Get user's immutable object id from claims that came from Azure AD
string oid = ctx.Principal.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");
//Get EF context
var db = ctx.HttpContext.RequestServices.GetRequiredService<AuthorizationDbContext>();
//Check is user a super admin
bool isSuperAdmin = await db.SuperAdmins.AnyAsync(a => a.ObjectId == oid);
if (isSuperAdmin)
{
//Add claim if they are
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "superadmin")
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
}
}
};
});
moving to part 3
parsing token in javascript
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(window.atob(base64));
};
parsing token in C#
use this libary https://www.jsonwebtoken.io/
try {
string jsonPayload = JWT.JsonWebToken.Decode(token, secretKey);
Console.WriteLine(jsonPayload);
} catch (JWT.SignatureVerificationException) {
Console.WriteLine("Invalid token!");
}
or manual
var jwtHandler = new JwtSecurityTokenHandler();
var jwtInput = txtJwtIn.Text;
//Check if readable token (string is in a JWT format)
var readableToken = jwtHandler.CanReadToken(jwtInput);
if(readableToken != true)
{
txtJwtOut.Text = "The token doesn't seem to be in a proper JWT format.";
}
if(readableToken == true)
{
var token = jwtHandler.ReadJwtToken(jwtInput);
//Extract the headers of the JWT
var headers = token.Header;
var jwtHeader = "{";
foreach(var h in headers)
{
jwtHeader += '"' + h.Key + "\":\"" + h.Value + "\",";
}
jwtHeader += "}";
txtJwtOut.Text = "Header:\r\n" + JToken.Parse(jwtHeader).ToString(Formatting.Indented);
//Extract the payload of the JWT
var claims = token.Claims;
var jwtPayload = "{";
foreach(Claim c in claims)
{
jwtPayload += '"' + c.Type + "\":\"" + c.Value + "\",";
}
jwtPayload += "}";
txtJwtOut.Text += "\r\nPayload:\r\n" + JToken.Parse(jwtPayload).ToString(Formatting.Indented);
}
I hope that answer your qustions