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
Related
I have three applications that all use different .NET methodologies (I don't know a better word off-hand). I have a .NET Core 3.1 Web App, a .NET Framework 4.8 MVC Web App, and a .NET Framework 4.6 Web Forms Application. All written in C#.
All three require that I use PKCE (Proof Key for Code Exchange). All three applications use Amazon AWS Cognito for Authentication and Authorization. Everything works well except that the two non-.NET Core apps don't use PKCE and I need them to.
Okay, so my main thinking here is that there might possibly be some "easy breezy" way I can add PKCE support to the .NET Framework applications via the Startup.Auth.cs file or some such were we define the OIDC Auth.
What I've done:
I've been able to find some online articles on how to add PKCE to a site, but they're all manual mechanisms of adding the necessary bits to the query string.
How to secure the Cognito login flow with a state nonce and PKCE
Authorize endpoint
ASP.NET Core using Proof Key for Code Exchange (PKCE)
The .NET Core article was particularly useful for my identification of how my .NET Core app works, but it doesn't help with the other two apps. Overall, the process seems to be broken down into the following steps:
Create a Code_Verifier Call it VERIFIER.
SHA256 that Code_Verifier and base 64 encode it. Call it the CHALLENGE.
Add this to the query string we send to the "authorize" endpoint along with a code_challenge_method which must, for Cognito, be set to S256.
Add to the query string that we send to the "token" endpoint, the Code_Verifier with the value VERIFIER.
In essence, we could manually add the following to the query string that calls the "authorize" endpoint: "&code_challenge=CHALLENGE&code_challenge_method=S256".
And to the query string that calls the "token" endpoint: "&code_verifier=VERIFIER". (I'm pretty sure this token endpoint is a POST.)
I think that's it. So, I could add this stuff manually, somehow, to these calls. However, I would prefer to let the .NET Framework magically do the work for me.
Hence, my overall question. Is there a way for me to modify the .NET code I have to have PKCE added by modifying my code without manually adding it.
I've searched the web, the Microsoft site, and I've fiddled around with the actual code, but I'm not finding any mechanisms for adding PKCE in these methods.
My Examples from my code:
.NET Framework Web Forms App:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieManager = new SystemWebCookieManager()
});
ConfigureIdentityProviders(app, DefaultAuthenticationTypes.ApplicationCookie);
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
var saveTokens = true;
var validateIssuer = true;
var saveTokensValue = ConfigurationManager.AppSettings["Cognito.SaveTokens"];
if (!string.IsNullOrEmpty(saveTokensValue))
{
saveTokens = bool.TryParse(saveTokensValue, out var outResult) && outResult;
}
var validateIssuerValue = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.ValidateIssuer"];
if (!string.IsNullOrEmpty(validateIssuerValue))
{
validateIssuer = bool.TryParse(validateIssuerValue, out var outResult) && outResult;
}
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationManager.AppSettings["Cognito.ClientId"],
ResponseType = ConfigurationManager.AppSettings["Cognito.ResponseType"],
Authority = ConfigurationManager.AppSettings["Cognito.Authority"],
MetadataAddress = ConfigurationManager.AppSettings["Cognito.MetadataAddress"],
ClientSecret = ConfigurationManager.AppSettings["Cognito.ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["Cognito.RedirectUri"],
SaveTokens = saveTokens,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.NameClaimType"],
RoleClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.RoleClaimType"],
ValidateIssuer = validateIssuer
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async (context) =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https",
Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
}
},
Scope = ConfigurationManager.AppSettings["Cognito.Scope"],
SignInAsAuthenticationType = signInAsType
}
);
}
My .NET Framework MVC App:
public void ConfigureAuth(IAppBuilder app)
{
var loginPath = AuthorizationSettings.Instance.LoginPath;
if (string.IsNullOrEmpty(loginPath))
throw new ArgumentNullException("No value specified for EiHubSettings LoginPath");
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(SecurityDbContext.Create);
app.CreatePerOwinContext<SecurityUserManager>(SecurityUserManager.Create);
app.CreatePerOwinContext<SecurityRoleManager>(SecurityRoleManager.Create);
app.CreatePerOwinContext<SecuritySignInManager>(SecuritySignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString(loginPath),
CookieManager = new SystemWebCookieManager(),
ExpireTimeSpan = TimeSpan.FromSeconds(
Convert.ToInt32(ConfigurationManager.AppSettings["Cognito.CookieLifetimeInSeconds"])),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<SecurityUserManager, SecurityUser, int>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.CreateIdentityAsync(manager),
getUserIdCallback: (user) => user.GetUserId<int>())
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
if(AppSettingsConfigSingleton.UsesCognito)
{
ConfigureIdentityProviders(app, DefaultAuthenticationTypes.ApplicationCookie);
}
}
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
var saveTokens = true;
var validateIssuer = true;
var saveTokensValue = ConfigurationManager.AppSettings["Cognito.SaveTokens"];
if (!string.IsNullOrEmpty(saveTokensValue))
{
saveTokens = bool.TryParse(saveTokensValue, out var outResult) && outResult;
}
var validateIssuerValue = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.ValidateIssuer"];
if (!string.IsNullOrEmpty(validateIssuerValue))
{
validateIssuer = bool.TryParse(validateIssuerValue, out var outResult) && outResult;
}
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationManager.AppSettings["Cognito.ClientId"],
ResponseType = ConfigurationManager.AppSettings["Cognito.ResponseType"],
Authority = ConfigurationManager.AppSettings["Cognito.Authority"],
MetadataAddress = ConfigurationManager.AppSettings["Cognito.MetadataAddress"],
ClientSecret = ConfigurationManager.AppSettings["Cognito.ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["Cognito.RedirectUri"],
SaveTokens = saveTokens,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = context =>
{
var redirectUri = "/account/openidlogincallback";
if (!string.IsNullOrEmpty(context.AuthenticationTicket.Properties.RedirectUri))
{
redirectUri += $"?returnUrl={context.AuthenticationTicket.Properties.RedirectUri}";
}
context.AuthenticationTicket.Properties.RedirectUri = redirectUri;
return Task.FromResult(0);
},
RedirectToIdentityProvider = context =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https", Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
return Task.FromResult(0);
}
},
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.NameClaimType"],
RoleClaimType = ConfigurationManager.AppSettings["Cognito.TokenValidationParameters.RoleClaimType"],
ValidateIssuer = validateIssuer
},
Scope = ConfigurationManager.AppSettings["Cognito.Scope"],
SignInAsAuthenticationType = signInAsType
}
);
}
The .NET Core 3.1 app (yes, I know we need to upgrade to .NET 6), has a "built-in" mechanism for adding PKCE (which befuddled me for the longest time until I saw this -- this option is not explicitly set in my code, but as you can see, it's by default turned on):
So, I'm hoping against hope, that there is something similar in the .NET Framework
In the StartUp class:
// Referred to in CustomOidcHandler
public const string CODE_VERIFIER = "code_verifier";
private const string _CODE_CHALLENGE = "code_challenge";
private const string _CODE_CHALLENGE_METHOD = "code_challenge_method";
private const string _CODE_CHALLENGE_METHOD_S256 = "S256";
In the ConfigureIdentityProviders in Startup
RedirectToIdentityProvider = (context) =>
{
var removePortOnRedirectIdentifierValue = ConfigurationManager.AppSettings["Cognito.RemovePortOnRedirectToIdentityProvider"];
var convertResult = bool.TryParse(removePortOnRedirectIdentifierValue, out var removePortOnRedirectIdentifier);
if (removePortOnRedirectIdentifier && convertResult)
{
var builder = new UriBuilder(context.ProtocolMessage.RedirectUri)
{
Scheme = "https",
Port = -1
};
context.ProtocolMessage.RedirectUri = builder.ToString();
}
//
// We need the code_verifier value passed to the CustomOidcHandler
// so that it can be added to the POST to the /token endpoint
//
string codeVerifier = GenerateCodeVerifier();
HttpCookie cookie = new HttpCookie(CODE_VERIFIER);
cookie.Value = codeVerifier;
HttpContext.Current.Response.Cookies.Add(cookie);
//
// Add the code_challenge to the GET request to Cognito
//
// Methodology found here: https://github.com/IdentityServer/IdentityServer4/issues/4874
//
string codeChallenge = GenerateCodeChallenge(codeVerifier);
context.ProtocolMessage.SetParameter(_CODE_CHALLENGE, codeChallenge);
context.ProtocolMessage.SetParameter(_CODE_CHALLENGE_METHOD, _CODE_CHALLENGE_METHOD_S256);
return Task.CompletedTask;
}
In my CustomOidcHandler file, the GetTokens method:
string codeVerifier = Request != null && Request.Cookies != null && Request.Cookies[Startup.CODE_VERIFIER] != null ? Request.Cookies[Startup.CODE_VERIFIER] : string.Empty;
Dictionary<string, string> post = new Dictionary<string, string>
{
{"client_id", options.ClientId},
{"client_secret", options.ClientSecret},
{"grant_type", "authorization_code"},
{"code", authorizationCode},
{"redirect_uri", options.RedirectUri},
{Startup.CODE_VERIFIER, codeVerifier}
};
Hence, the only way is to "manually" add it.
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.
We have developed a set of Web APIs (REST) which are protected by an Authorization server. The Authorization server has issued the client id and client secret. These can be used to obtain an access token. A valid token can be used on subsequent calls to resource servers (REST APIs).
I want to write an web based (Asp.net MVC 5) client that will consume the APIs. Is there a nuget package I can download that will help me to implement the client OAuth2 flow? Can anyone direct me to a good example on client implementation of OAuth2 flow (written in asp.net MVC)?
Update
I was able to get access token using the code block below, but what I want is a "client credentials" oauth 2 flow where I don't have to enter login and passwords. The code I have now is:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType("ClientCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = "ClientCookie",
CookieName = CookieAuthenticationDefaults.CookiePrefix + "ClientCookie",
ExpireTimeSpan = TimeSpan.FromMinutes(5)
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(),
ClientId = ConfigurationManager.AppSettings["AuthServer:ClientId"],
ClientSecret = ConfigurationManager.AppSettings["AuthServer:ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["AuthServer:RedirectUrl"],
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://identityserver.com/oauth2/authorize",
TokenEndpoint = "https://identityserver.com/oauth2/token"
},
//ResponseType = "client_credentials", // Doesn't work
ResponseType = "token",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = notification =>
{
if (string.Equals(notification.ProtocolMessage.Error, "access_denied", StringComparison.Ordinal))
{
notification.HandleResponse();
notification.Response.Redirect("/");
}
return Task.FromResult<object>(null);
},
AuthorizationCodeReceived = async notification =>
{
using (var client = new HttpClient())
{
//var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
String tokenEndPoint = "https://identityserver.com/oauth2/token";
//var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint);
var request = new HttpRequestMessage(HttpMethod.Post, tokenEndPoint);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ OpenIdConnectParameterNames.ClientId, notification.Options.ClientId },
{ OpenIdConnectParameterNames.ClientSecret, notification.Options.ClientSecret },
{ OpenIdConnectParameterNames.Code, notification.ProtocolMessage.Code },
{ OpenIdConnectParameterNames.GrantType, "authorization_code" },
{ OpenIdConnectParameterNames.RedirectUri, notification.Options.RedirectUri }
});
var response = await client.SendAsync(request, notification.Request.CallCancelled);
response.EnsureSuccessStatusCode();
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
// Add the access token to the returned ClaimsIdentity to make it easier to retrieve.
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: OpenIdConnectParameterNames.AccessToken,
value: payload.Value<string>(OpenIdConnectParameterNames.AccessToken)));
}
}
}
});
}
}
To support the client credentials grant type, your best option is probably to directly use HttpClient:
var request = new HttpRequestMessage(HttpMethod.Post, "http://server.com/token");
request.Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "client_id", "your client_id" },
{ "client_secret", "your client_secret" },
{ "grant_type", "client_credentials" }
});
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
var token = payload.Value<string>("access_token");
For interactive flows (like the authorization code flow), there are two better approaches:
If your authorization server supports OpenID Connect (which is based on OAuth2), you can simply use the OpenID Connect middleware for OWIN/Katana 3 developed by Microsoft: https://www.nuget.org/packages/Microsoft.Owin.Security.OpenIdConnect/
If OpenID Connect is not supported by your authorization server, one option is to create your own OAuth2 client middleware. You can take a look at the last part of this SO answer for more information: Registering Web API 2 external logins from multiple API clients with OWIN Identity
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've got a Web API project fronted by Angular, and I want to secure it using a JWT token. I've already got user/pass validation happening, so I think i just need to implement the JWT part.
I believe I've settled on JwtAuthForWebAPI so an example using that would be great.
I assume any method not decorated with [Authorize] will behave as it always does, and that any method decorated with [Authorize] will 401 if the token passed by the client doesn't match.
What I can't yet figure out it how to send the token back to the client upon initial authentication.
I'm trying to just use a magic string to begin, so I have this code:
RegisterRoutes(GlobalConfiguration.Configuration.Routes);
var builder = new SecurityTokenBuilder();
var jwtHandler = new JwtAuthenticationMessageHandler
{
AllowedAudience = "http://xxxx.com",
Issuer = "corp",
SigningToken = builder.CreateFromKey(Convert.ToBase64String(new byte[]{4,2,2,6}))
};
GlobalConfiguration.Configuration.MessageHandlers.Add(jwtHandler);
But I'm not sure how that gets back to the client initially. I think I understand how to handle this on the client, but bonus points if you can also show the Angular side of this interaction.
I ended-up having to take a information from several different places to create a solution that works for me (in reality, the beginnings of a production viable solution - but it works!)
I got rid of JwtAuthForWebAPI (though I did borrow one piece from it to allow requests with no Authorization header to flow through to WebAPI Controller methods not guarded by [Authorize]).
Instead I'm using Microsoft's JWT Library (JSON Web Token Handler for the Microsoft .NET Framework - from NuGet).
In my authentication method, after doing the actual authentication, I create the string version of the token and pass it back along with the authenticated name (the same username passed into me, in this case) and a role which, in reality, would likely be derived during authentication.
Here's the method:
[HttpPost]
public LoginResult PostSignIn([FromBody] Credentials credentials)
{
var auth = new LoginResult() { Authenticated = false };
if (TryLogon(credentials.UserName, credentials.Password))
{
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, credentials.UserName),
new Claim(ClaimTypes.Role, "Admin")
}),
AppliesToAddress = ConfigurationManager.AppSettings["JwtAllowedAudience"],
TokenIssuerName = ConfigurationManager.AppSettings["JwtValidIssuer"],
SigningCredentials = new SigningCredentials(new
InMemorySymmetricSecurityKey(JwtTokenValidationHandler.SymmetricKey),
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
"http://www.w3.org/2001/04/xmlenc#sha256")
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
auth.Token = tokenString;
auth.Authenticated = true;
}
return auth;
}
UPDATE
There was a question about handling the token on subsequent requests. What I did was create a DelegatingHandler to try and read/decode the token, then create a Principal and set it into Thread.CurrentPrincipal and HttpContext.Current.User (you need to set it into both). Finally, I decorate the controller methods with the appropriate access restrictions.
Here's the meat of the DelegatingHandler:
private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
{
token = null;
IEnumerable<string> authzHeaders;
if (!request.Headers.TryGetValues("Authorization", out authzHeaders) || authzHeaders.Count() > 1)
{
return false;
}
var bearerToken = authzHeaders.ElementAt(0);
token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
return true;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpStatusCode statusCode;
string token;
var authHeader = request.Headers.Authorization;
if (authHeader == null)
{
// missing authorization header
return base.SendAsync(request, cancellationToken);
}
if (!TryRetrieveToken(request, out token))
{
statusCode = HttpStatusCode.Unauthorized;
return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
}
try
{
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters =
new TokenValidationParameters()
{
AllowedAudience = ConfigurationManager.AppSettings["JwtAllowedAudience"],
ValidIssuer = ConfigurationManager.AppSettings["JwtValidIssuer"],
SigningToken = new BinarySecretSecurityToken(SymmetricKey)
};
IPrincipal principal = tokenHandler.ValidateToken(token, validationParameters);
Thread.CurrentPrincipal = principal;
HttpContext.Current.User = principal;
return base.SendAsync(request, cancellationToken);
}
catch (SecurityTokenValidationException e)
{
statusCode = HttpStatusCode.Unauthorized;
}
catch (Exception)
{
statusCode = HttpStatusCode.InternalServerError;
}
return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
}
Don't forget to add it into the MessageHandlers pipeline:
public static void Start()
{
GlobalConfiguration.Configuration.MessageHandlers.Add(new JwtTokenValidationHandler());
}
Finally, decorate your controller methods:
[Authorize(Roles = "OneRoleHere")]
[GET("/api/admin/settings/product/allorgs")]
[HttpGet]
public List<Org> GetAllOrganizations()
{
return QueryableDependencies.GetMergedOrganizations().ToList();
}
[Authorize(Roles = "ADifferentRoleHere")]
[GET("/api/admin/settings/product/allorgswithapproval")]
[HttpGet]
public List<ApprovableOrg> GetAllOrganizationsWithApproval()
{
return QueryableDependencies.GetMergedOrganizationsWithApproval().ToList();
}