We have several tests that that generate a jwt request to call a server to retrieve a token. We have 6 tests that make the same call to the same method using the same data. Here is the method:
'''
private static string GenerateSignedTokenRequest(
string privateKey,
string privateKeyPass,
string clientID,
string audience,
int lifetime)
{
var jti = Guid.NewGuid().ToString();
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Jti, jti),
new Claim(JwtRegisteredClaimNames.Sub, clientID),
};
var decodedKey = DecodeRsaPrivateKeyFromPem(
privateKey,
privateKeyPass);
var priDecKey = decodedKey.Private as RsaPrivateCrtKeyParameters;
var rsaParams = DotNetUtilities.ToRSAParameters(priDecKey);
using (var rsa = RSA.Create(rsaParams))
{
var token = new JwtSecurityToken(
clientID,
audience,
claims,
DateTime.Now.AddMinutes(-1),
DateTime.Now.AddSeconds(lifetime),
new SigningCredentials(
new RsaSecurityKey(rsa),
SecurityAlgorithms.RsaSha256));
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
'''
We get the following error on every other test that runs on the WriteToken(token) method:
{"Cannot access a disposed object.\r\nObject name: 'RSA'."}
What is baffling is each odd number test runs through this code fine but each even number test fails. But when I rerun each test individually they are all green. It is only when I run them all together that every other test fails.
This has happened when moving from .Net Core and Test frameworks from 3.1.0 to 3.1.4
So It seems the issue was the upgrade of Windows Azure Active Directory IdentityModel Extensions for .Net. It seems there is a cache that not is affected by putting a using around the RSA.Create() method. By removing the using all the tests are green.
here are a few links that helped my diagnose:
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc
And:
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1433
Update this
new SigningCredentials(
new RsaSecurityKey(rsa),
SecurityAlgorithms.RsaSha256)
To
new SigningCredentials(
new RsaSecurityKey(rsa),
SecurityAlgorithms.RsaSha256){
CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
}
reference : https://vmsdurano.com/-net-core-3-1-signing-jwt-with-rsa/
Below code works perfectly fine for me.
In the below code, I am taking claims as parameter and pulling certificate from configuration. Instead of setting private key to a variable, putting directly in key fixed the issue.
using (X509Certificate2 certificate = new X509Certificate2(certPath, tokenConfig["CertSecret"], System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.PersistKeySet))
{
if (certificate.HasPrivateKey)
{
var signingCredentials = new SigningCredentials(
key: new RsaSecurityKey((RSA)certificate.PrivateKey),
algorithm: SecurityAlgorithms.RsaSha256
);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.Now.AddMinutes(Convert.ToInt32(configuration["TokenMinutesToExpire"])),
signingCredentials: signingCredentials);
return $"Bearer {new JwtSecurityTokenHandler().WriteToken(token)}";
}
}
}
Related
I can't get the simple Web Api authorized request via JWT in NET 7 minimal API from a MAUI client app (see below).
Therefore I thought of a workaround solution which looks like this:
from the client side I also send the user data (user and password) for each request:
UserAuthentication user = new UserAuthentication();
user.UserName = "user1";
user.Email = "user1#email.com";
user.Password = "321";
HttpClient httpClient = new HttpClient();
var response = await
httpClient.PostAsJsonAsync("https://api.mywebsite.com/api/data", user);
in the minimal api, I then check the user details at the endpoint and then send the data back if the login matches.
app.MapPost("/api/data",
[AllowAnonymous] (User user) =>.
{
if (user.UserName == "user1" && user.Password == "321")
{
return Results.Ok(list_data);
}
return Results.Unauthorized();
});
Two remarks:
in the release version both user and password should be encrypted
of course the user data comes from the DB, I hard-coded the user here for testing.
Can anyone confirm that I can implement an authorized request like this (i.e. without JWT)? Or have I missed something important here with my reasoning? The important thing is that it is at least as secure as with JWT.
Here again examples that do not work in MAUI Client (are already in other posts of mine). I'm starting to think there's a problem here at Microsoft, but don't know where best to report the problem (did it at ASP-NET-CORE, got rejected https://github.com/dotnet/AspNetCore.Docs/issues/27929 ).
minimal API (NET 7):
app.MapGet("/secret2", [Authorize] () => $"Hello You. This is a secret!!!");
MAUI CLient:
HttpClient httpClient = new HttpClient();
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(#"https://api.mysite.com/secret2")
};
requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = await httpClient.SendAsync(requestMessage);
Although the same JWT works via Postman, here in MAUI I get an Unauthorized message 401 when requesting. I already posted details about this (also network protocol), see also github link.
Thanks
EDIT
According to Tiny Wang, the client code works. I then assume that there is an error in the JWT generation (wondering how the generated token then worked via Postman).
Here is the completely code from Web Api (for the generation and request of JWT, as well as endpoint for a request via JWT).).
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace WebApplication1
{
public class User
{
public string UserName { get; set; } = "";
public string Email { get; set; } = "";
public string Password { get; set; } = "";
public string AddInfo { get; set; } = "";
}
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddAuthentication().AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey#345"))
};
});
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/api/test", [AllowAnonymous] () => "Hello you!");
app.MapGet("/secret2", [Authorize] () => $"Hello You. This is a secret!!!");
app.MapPost("/security/createToken",
[AllowAnonymous] (User user) =>
{
if (user.UserName == "user" && user.Password == "123")
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
new Claim(JwtRegisteredClaimNames.GivenName, user.UserName),
new Claim(JwtRegisteredClaimNames.Email, "user#test.com"),
new Claim(ClaimTypes.Role, "Administrator"),
new Claim("Role1", "Administrator"),
new Claim("Role2", "Standard"),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey#345"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: "https://api.mysite.com:64591",
audience: "https://api.mysite.com:64591",
claims: claims,
expires: DateTime.Now.AddMinutes(50),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return Results.Ok(tokenString);
}
return Results.Unauthorized();
});
app.UseHttpsRedirection();
app.Run();
}
}
}
The cause of the problem was tricky, because even the many tutorials and posts in the form as they are given in Internet will not work. But if you copy generated token out (e.g. from debug mode) and use it in Postman, then everything will work nicely and this is something that confuses you a lot. Fortunately, there are still people who have incredible mind and can detect such inconsistencies. I wouldn't have seen this in 1000 years either :)
See: https://learn.microsoft.com/en-us/answers/questions/1133200/401-unauthorized-consuming-web-api-with-jwt-authen.html
I am using
var payload = new JwtPayload(issuer, audience, claim identities, DateTime.Now, DateTime.Now.AddMinutes(60), DateTime.Now);
to generate auth token having 60-minute expiring time but it is expiring before the expiring time
I use the following method to build a token in my asp.net core web api service:
private string BuildToken()
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.Key));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
_jwtOptions.Issuer, // some issuer, e.x. you can specify your localhost
_jwtOptions.Issuer,
expires: DateTime.Now.AddMinutes(_jwtOptions.Expires), // int value
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Perhaps this helps you somehow.
I'm trying to get calendars from Office 365 to use them in a REST API (WEB API 2).
I already tried a lot of stuff to generate the JWT, but for each try, I get another error.
My application is properly registred in Azure AAD, the public key is uploaded.
The last thing I tried was from this article : https://blogs.msdn.microsoft.com/exchangedev/2015/01/21/building-daemon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow/
In his example, I can generate the JWT from two differents ways, but I get the error : x-ms-diagnostics: 2000003;reason="The audience claim value is invalid 'https://outlook.office365.com'.";error_category="invalid_resource"
Here is my code :
`
string tenantId = ConfigurationManager.AppSettings.Get("ida:TenantId");
/**
* use the tenant specific endpoint for requesting the app-only access token
*/
string tokenIssueEndpoint = "https://login.windows.net/" + tenantId + "/oauth2/authorize";
string clientId = ConfigurationManager.AppSettings.Get("ida:ClientId");
/**
* sign the assertion with the private key
*/
String certPath = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/cert.pfx");
X509Certificate2 cert = new X509Certificate2(
certPath,
"lol",
X509KeyStorageFlags.MachineKeySet);
/**
* Example building assertion using Json Tokenhandler.
* Sort of cheating, but just if someone wonders ... there are always more ways to do something :-)
*/
Dictionary<string, string> claims = new Dictionary<string, string>()
{
{ "sub", clientId },
{ "jti", Guid.NewGuid().ToString() },
};
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
X509SigningCredentials signingCredentials = new X509SigningCredentials(cert, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest);
JwtSecurityToken selfSignedToken = new JwtSecurityToken(
clientId,
tokenIssueEndpoint,
claims.Select(c => new Claim(c.Key, c.Value)),
DateTime.UtcNow,
DateTime.UtcNow.Add(TimeSpan.FromMinutes(15)),
signingCredentials);
string signedAssertion = tokenHandler.WriteToken(selfSignedToken);
//---- End example with Json Tokenhandler... now to the fun part doing it all ourselves ...
/**
* Example building assertion from scratch with Crypto APIs
*/
JObject clientAssertion = new JObject();
clientAssertion.Add("aud", "https://outlook.office365.com");
clientAssertion.Add("iss", clientId);
clientAssertion.Add("sub", clientId);
clientAssertion.Add("jti", Guid.NewGuid().ToString());
clientAssertion.Add("scp", "Calendars.Read");
clientAssertion.Add("nbf", WebConvert.EpocTime(DateTime.UtcNow + TimeSpan.FromMinutes(-5)));
clientAssertion.Add("exp", WebConvert.EpocTime(DateTime.UtcNow + TimeSpan.FromMinutes(15)));
string assertionPayload = clientAssertion.ToString(Newtonsoft.Json.Formatting.None);
X509AsymmetricSecurityKey x509Key = new X509AsymmetricSecurityKey(cert);
RSACryptoServiceProvider rsa = x509Key.GetAsymmetricAlgorithm(SecurityAlgorithms.RsaSha256Signature, true) as RSACryptoServiceProvider;
RSACryptoServiceProvider newRsa = GetCryptoProviderForSha256(rsa);
SHA256Cng sha = new SHA256Cng();
JObject header = new JObject(new JProperty("alg", "RS256"));
string thumbprint = WebConvert.Base64UrlEncoded(WebConvert.HexStringToBytes(cert.Thumbprint));
header.Add(new JProperty("x5t", thumbprint));
string encodedHeader = WebConvert.Base64UrlEncoded(header.ToString());
string encodedPayload = WebConvert.Base64UrlEncoded(assertionPayload);
string signingInput = String.Concat(encodedHeader, ".", encodedPayload);
byte[] signature = newRsa.SignData(Encoding.UTF8.GetBytes(signingInput), sha);
signedAssertion = string.Format("{0}.{1}.{2}",
encodedHeader,
encodedPayload,
WebConvert.Base64UrlEncoded(signature));
`
My JWT looks like this :
`
{
alg: "RS256",
x5t: "8WkmVEiCU9mHkshRp65lyowGOAk"
}.
{
aud: "https://outlook.office365.com",
iss: "clientId",
sub: "clientId",
jti: "38a34d8a-0764-434f-8e1d-c5774cf37007",
scp: "Calendars.Read",
nbf: 1512977093,
exp: 1512978293
}
`
I put this token in the Authorization header after the "Bearer" string.
Any ideas to solve this kind of issue ? I guess I need a external point of view :)
Thanks
You do not generate the JWT, Azure AD does that.
You would use your certificate to get the access token. Example borrowed from article you linked:
string authority = appConfig.AuthorizationUri.Replace("common", tenantId);
AuthenticationContext authenticationContext = new AuthenticationContext(
authority,
false);
string certfile = Server.MapPath(appConfig.ClientCertificatePfx);
X509Certificate2 cert = new X509Certificate2(
certfile,
appConfig.ClientCertificatePfxPassword, // password for the cert file containing private key
X509KeyStorageFlags.MachineKeySet);
ClientAssertionCertificate cac = new ClientAssertionCertificate(
appConfig.ClientId, cert);
var authenticationResult = await authenticationContext.AcquireTokenAsync(
resource, // always https://outlook.office365.com for Mail, Calendar, Contacts API
cac);
return authenticationResult.AccessToken;
The resulting access token can then be attached to the request to the API.
The reason it does not work is that the Outlook API does not consider you a valid token issuer. It will only accept tokens signed with Azure AD's private key. Which you obviously do not have.
The private key from the key pair you generated can only be used to authenticate your app to Azure AD.
Thanks juunas !
This is the working code :
var authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext("https://login.microsoftonline.com/tenantId");
string tenantId = ConfigurationManager.AppSettings.Get("ida:TenantId");
string clientId = ConfigurationManager.AppSettings.Get("ida:ClientId");
String certPath = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/cert.pfx");
X509Certificate2 cert = new X509Certificate2(
certPath,
"keyPwd",
X509KeyStorageFlags.MachineKeySet);
ClientAssertionCertificate cac = new ClientAssertionCertificate(clientId, cert);
var result = (AuthenticationResult)authContext
.AcquireTokenAsync("https://outlook.office.com", cac)
.Result;
var token = result.AccessToken;
return token;
Other required step for App-only token, you must use the Grant Permissions button in AAD application settings.
There are millions of guides out there, and none of them seem to do what I need. I am creating an Authentication Server, that simply just needs to issue, and validate/reissue tokens. So I can't create a middleware class to "VALIDATE" the cookie or header. I am simply receiving a POST of the string, and I need to validate the token that way, instead of the Authorize middleware that .net core provides.
My Startup Consists of the only Token Issuer Example I could get working.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseExceptionHandler("/Home/Error");
app.UseStaticFiles();
var secretKey = "mysupersecret_secretkey!123";
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
var options = new TokenProviderOptions
{
// The signing key must match!
Audience = "AllApplications",
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
Issuer = "Authentication"
};
app.UseMiddleware<TokenProviderMiddleware>(Microsoft.Extensions.Options.Options.Create(options));
I can use the middleware on creation since I just need to intercept the body for the username and password. The middleware takes in the options from the previous Startup.cs code, checks the Request Path and will Generate the token from the context seen below.
private async Task GenerateToken(HttpContext context)
{
CredentialUser usr = new CredentialUser();
using (var bodyReader = new StreamReader(context.Request.Body))
{
string body = await bodyReader.ReadToEndAsync();
usr = JsonConvert.DeserializeObject<CredentialUser>(body);
}
///get user from Credentials put it in user variable. If null send bad request
var now = DateTime.UtcNow;
// Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
// You can add other claims here, if you want:
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, JsonConvert.SerializeObject(user)),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, now.ToString(), ClaimValueTypes.Integer64)
};
// Create the JWT and write it to a string
var jwt = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
notBefore: now,
expires: now.Add(_options.Expiration),
signingCredentials: _options.SigningCredentials);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
///fill response with jwt
}
This large block of code above will Deserialize the CredentialUser json and then execute a stored procedure that returns the User Object. I will then add three claims, and ship it back.
I am able to successfully generate a jwt, and using an online tool like jwt.io, I put the secret key, and the tool says it is valid, with an object that I could use
{
"sub": " {User_Object_Here} ",
"jti": "96914b3b-74e2-4a68-a248-989f7d126bb1",
"iat": "6/28/2017 4:48:15 PM",
"nbf": 1498668495,
"exp": 1498668795,
"iss": "Authentication",
"aud": "AllApplications"
}
The problem I'm having is understanding how to manually check the claims against the signature. Since this is a server that issues and validates tokens. Setting up the Authorize middleware is not an option, like most guides have. Below I am attempting to Validate the Token.
[Route("api/[controller]")]
public class ValidateController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Validate(string token)
{
var validationParameters = new TokenProviderOptions()
{
Audience = "AllMyApplications",
SigningCredentials = new
SigningCredentials("mysupersecret_secretkey!123",
SecurityAlgorithms.HmacSha256),
Issuer = "Authentication"
};
var decodedJwt = new JwtSecurityTokenHandler().ReadJwtToken(token);
var valid = new JwtSecurityTokenHandler().ValidateToken(token, //The problem is here
/// I need to be able to pass in the .net TokenValidParameters, even though
/// I have a unique jwt that is TokenProviderOptions. I also don't know how to get my user object out of my claims
}
}
I stole borrowed this code mostly from the ASP.Net Core source code: https://github.com/aspnet/Security/blob/dev/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L45
From that code I created this function:
private string Authenticate(string token) {
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
List<Exception> validationFailures = null;
SecurityToken validatedToken;
var validator = new JwtSecurityTokenHandler();
// These need to match the values used to generate the token
TokenValidationParameters validationParameters = new TokenValidationParameters();
validationParameters.ValidIssuer = "http://localhost:5000";
validationParameters.ValidAudience = "http://localhost:5000";
validationParameters.IssuerSigningKey = key;
validationParameters.ValidateIssuerSigningKey = true;
validationParameters.ValidateAudience = true;
if (validator.CanReadToken(token))
{
ClaimsPrincipal principal;
try
{
// This line throws if invalid
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
// If we got here then the token is valid
if (principal.HasClaim(c => c.Type == ClaimTypes.Email))
{
return principal.Claims.Where(c => c.Type == ClaimTypes.Email).First().Value;
}
}
catch (Exception e)
{
_logger.LogError(null, e);
}
}
return String.Empty;
}
The validationParameters need to match those in your GenerateToken function and then it should validate just fine.
I am trying to setup an IdentityServer 3 web application not hardware, this is a software development related question. I am trying to learn how to use the technology and produce JWT token's that my api can consume. The problem is I cannot for the life of me find where to set the token expiration. It always produces a 401 after about an hour. Ideally for testing purposes I would like to extend this to a very long time so I do not have to keep copy and pasting my JWT token into fiddler thus dramatically slowing down my development and learning process.
My Client
new Client
{
ClientId = "scheduling"
,ClientSecrets = new List<Secret>
{
new Secret("65A6A6C3-A764-41D9-9D10-FC09E0DBB046".Sha256())
},
ClientName = "Patient Scheduling",
Flow = Flows.ResourceOwner,
AllowedScopes = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.OfflineAccess,
"read",
"adprofile",
"scheduling"
},
Enabled = true
}
My Scope
new Scope
{
Name = "scheduling",
Claims = new List<ScopeClaim>
{
new ScopeClaim(Constants.ClaimTypes.Role,true),
new ScopeClaim("scheduling_id",true),
new ScopeClaim("expires_at",true) //I have tried "expires_in" and [Constants.ClaimTypes.Expiration] also with no luck
}
}
Method used for client specific claims:
private IEnumerable<Claim> GetClaimByClientId(string client_id)
{
List<Claim> claims = new List<Claim>();
switch(client_id.ToLower())
{
case "scheduling":
claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Role,"administrator"));
claims.Add(new Claim("scheduling_id", "2"));
//claims.Add(new Claim("expires_in", "2082758400")); //01/01/2036
//claims.Add(new Claim(Constants.ClaimTypes.Expiration, "2082758400")); //01/01/2036
claims.Add(new Claim("expires_at", "2082758400")); //01/01/2036
break;
default:
throw new Exception("Client not found with provided client id.");
}
return claims;
}
Code actually validating Credentials:
if (ActiveDirectoryHelper.ValidateCredentials(context.UserName, context.Password, adName))
{
List<Claim> lstClaims = new List<Claim>
{
new Claim("obj_id",user.UserID.ToUpper()),
new Claim(Constants.ClaimTypes.Email, string.IsNullOrEmpty(user.Email) ? string.Empty : user.Email.ToLower()),
new Claim(Constants.ClaimTypes.GivenName,user.FirstName),
new Claim(Constants.ClaimTypes.FamilyName,user.LastName),
new Claim("EmployeeNumber",user.EmployeeNumber),
};
lstClaims.AddRange(GetClaimByClientId("scheduling"));
context.AuthenticateResult = new AuthenticateResult(user.UserID,user.Username, lstClaims);
}
else
{
context.AuthenticateResult = new AuthenticateResult("Invalid Login.");
}
Access Token lifetime (I assume this is what you mean by JWT token) can be set for a client application using the Client property AccessTokenLifetime.
By default this is set to 3600 seconds (1 hour).