invalid signature - JWT is required to have three segments - c#

I am using OAuthAuthorizationServerProvider from Microsoft Owin Security and here is I am using code,
var oAuthAuthorizationServerOptions = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5),
AllowInsecureHttp = true,
Provider = new CustomOAuthProvider()
};
CustomOAuthProvider,
public class CustomOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
var lstClients = ClientService.GetClients();
if (lstClients.Count <= 0) return base.ValidateClientAuthentication(context);
context.TryGetFormCredentials(out var clientId, out var clientSecret);
if (lstClients.Count(c => c.ClientId == clientId) > 0
&& lstClients.Count(c => c.ClientPassword == clientSecret) > 0)
{
context.Validated(clientId);
}
return base.ValidateClientAuthentication(context);
}
public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
var claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.ClientId));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{ "client_id", context.ClientId },
{ "scope", string.Join(" ",context.Scope) }
});
var ticket = new AuthenticationTicket(claimsIdentity, props);
context.Validated(ticket);
return base.GrantClientCredentials(context);
}
}
I am here trying to add scope, but looks like this is not correct way to add, even all looks good and working and when I am trying to view token,
in jwt.IO I am seeing invalid signature error.
in calebb.net, it's saying - JWT is required to have three segments
What's wrong here? Please suggest.

For a JWT token to be valid, you have to have three segments as your error message says.
According to the documentation:
In its compact form, JSON Web Tokens consist of three parts separated
by dots (.), which are:
Header
Payload
Signature
Therefore, a JWT typically looks like the
following.
xxxxx.yyyyy.zzzzz
You have to examine the code of your custom provider to ensure that those three segments are actually present. They have to be separated by a dot.

Related

Web Api Authentication without JWT (NET 7/ MAUI / minimal API: )?

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

How Do I Manually Validate a JWT Asp.Net Core?

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.

How to customize oAuth token providers in asp.net web api

I want to change the way the default bearer token system works.
I want to login to the webAPI providing the username, password, and mac address of the device. Like so.
Content-Type: application/x-www-form-urlencoded
username=test&password=P#ssword&grant_type=password&client_id=android&device_info=MAC_Address
I then want the API to provide me with a Refresh Token. This token will be valid for say 7 days and will allow for me to get a new access token. However in the refresh token I want to save / embed the security stamp of the users password in the token along with the extirpation date. This way I can check the security stamp when a new access token is requested. (solves password changing scenario)
My access token only needs to store the bare amount of information for it to work. I don't require that the access token store anything specific. I would like to keep it as small as possible. When it expires I will simply request a new access token using my refresh token.
Now I have tried to implement the above but have got my self heavily confused about what to implement where. Here's what i have got.
Step 1: The Startup.Auth.cs
//Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/Token"),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider(),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(20)
};
Now in here I already have some questions. I want to have two providers, one which handles Refresh Tokens and one that handles Access Tokens. Which providers do I need to set? because I see there is also one called AccessTokenProvider = then what is Provider = for?
Step 2: The RereshTokenProvider. This is what I have so far:
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
//Used to store all the refresh tokens
public static ConcurrentDictionary<string, AuthenticationTicket> RefreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();
public Task CreateAsync(AuthenticationTokenCreateContext context)
{
var guid = Guid.NewGuid().ToString("N");
//copy properties and set the desired lifetime of refresh token
var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
{
IssuedUtc = context.Ticket.Properties.IssuedUtc,
ExpiresUtc = DateTime.UtcNow.AddDays(7)
};
//TODO: get mac address from the request headers??
//TODO: save the mac address to db along with user and date
var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);
RefreshTokens.TryAdd(guid, refreshTokenTicket);
context.SetToken(guid);
return Task.FromResult<object>(null);
}
public Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
AuthenticationTicket ticket;
if (RefreshTokens.TryRemove(context.Token, out ticket))
{
context.SetTicket(ticket);
}
return Task.FromResult<object>(null);
}
public void Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
}
public void Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
}
}
Now if i understand correctly. The purpose of the SimpleRefreshTokenProvider is to build up a RefreshToken and to the validate it when the api receives a request with one in it?
Step 3: SimpleAuthorizationServerProvider. This is what I have so far. but I have a feeling this is where I have gone wrong. Or im getting confused, What is the purpose of this class? Is it not to validate the AccessToken?
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Not concerned about clients yet
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// validate user credentials
var userManager = context.OwinContext.GetUserManager<FskUserManager>();
FskUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
// create identity
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
//Set properties of the token
// create metadata to pass on to refresh token provider
AuthenticationProperties properties = new AuthenticationProperties(new Dictionary<string, string>
{
{"userName", user.UserName}
});
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
public override async Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
string originalClient;
context.Ticket.Properties.Dictionary.TryGetValue("as:client_id", out originalClient);
var currentClient = context.ClientId;
// chance to change authentication ticket for refresh token requests
var newId = new ClaimsIdentity(context.Ticket.Identity);
newId.AddClaim(new Claim("newClaim", "refreshToken"));
var newTicket = new AuthenticationTicket(newId, context.Ticket.Properties);
context.Validated(newTicket);
}
}
Please what am I missing here?

OWIN AuthenticationOptions updating at runtime in mvc5 application

Hi!
Here is the situation:
I have one MVC5 application with Identity2 on iis7 wich serves multiple web sites.
host name is the key for certain web site.
site.com,
anothersite.com
and so on
i've decided to use external login with google on all my sites and every site should be google client with personal clientid and clientsecret.
for example:
site.com - clientid=123123, clientsecret=xxxaaabbb
anothersite.com - clientid=890890, clientsecret=zzzqqqeee
but there is a little problem --
AuthenticationOptions are set at the start of application and i did'n find any way to replace it at runtime.
so, after reading Creating Custom OAuth Middleware for MVC 5
and Writing an Owin Authentication Middleware
i've realized that i should override AuthenticationHandler.ApplyResponseChallengeAsync()
and put this piece of code in the begining of this method:
Options.ClientId = OAuth2Helper.GetProviderAppId("google");
Options.ClientSecret = OAuth2Helper.GetProviderAppSecret("google");
i've decided to use only google, so we will talk about google middleware.
AuthenticationHandler are returned by AuthenticationMiddleWare.CreateHandler() and in my case they are GoogleOAuth2AuthenticationHandler and GoogleOAuth2AuthenticationMiddleware.
I've found GoogleOAuth2AuthenticationMiddleware at the http://katanaproject.codeplex.com/
and take it in my project like this
public class GoogleAuthenticationMiddlewareExtended : GoogleOAuth2AuthenticationMiddleware
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
public GoogleAuthenticationMiddlewareExtended(
OwinMiddleware next,
IAppBuilder app,
GoogleOAuth2AuthenticationOptions options)
: base(next, app, options)
{
_logger = app.CreateLogger<GoogleOAuth2AuthenticationMiddleware>();
_httpClient = new HttpClient(ResolveHttpMessageHandler(Options));
_httpClient.Timeout = Options.BackchannelTimeout;
_httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
}
protected override AuthenticationHandler<GoogleOAuth2AuthenticationOptions> CreateHandler()
{
return new GoogleOAuth2AuthenticationHandlerExtended(_httpClient, _logger);
}
private static HttpMessageHandler ResolveHttpMessageHandler(GoogleOAuth2AuthenticationOptions options)
{
HttpMessageHandler handler = options.BackchannelHttpHandler ?? new WebRequestHandler();
// If they provided a validator, apply it or fail.
if (options.BackchannelCertificateValidator != null)
{
// Set the cert validate callback
var webRequestHandler = handler as WebRequestHandler;
if (webRequestHandler == null)
{
throw new InvalidOperationException("Exception_ValidatorHandlerMismatch");
}
webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate;
}
return handler;
}
}
then i've create my own Handler with modified ApplyResponseChallengeAsync. i've got a bad news at this point - GoogleOAuth2AuthenticationHandler is internal and i had to take it entirely and put in my project like this (again katanaproject.codeplex.com)
public class GoogleOAuth2AuthenticationHandlerExtended : AuthenticationHandler<GoogleOAuth2AuthenticationOptions>
{
private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";
private const string UserInfoEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo?access_token=";
private const string AuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth";
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
public GoogleOAuth2AuthenticationHandlerExtended(HttpClient httpClient, ILogger logger)
{
_httpClient = httpClient;
_logger = logger;
}
// i've got some surpises here
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
AuthenticationProperties properties = null;
try
{
string code = null;
string state = null;
IReadableStringCollection query = Request.Query;
IList<string> values = query.GetValues("code");
if (values != null && values.Count == 1)
{
code = values[0];
}
values = query.GetValues("state");
if (values != null && values.Count == 1)
{
state = values[0];
}
properties = Options.StateDataFormat.Unprotect(state);
if (properties == null)
{
return null;
}
// OAuth2 10.12 CSRF
if (!ValidateCorrelationId(properties, _logger))
{
return new AuthenticationTicket(null, properties);
}
string requestPrefix = Request.Scheme + "://" + Request.Host;
string redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath;
// Build up the body for the token request
var body = new List<KeyValuePair<string, string>>();
body.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
body.Add(new KeyValuePair<string, string>("code", code));
body.Add(new KeyValuePair<string, string>("redirect_uri", redirectUri));
body.Add(new KeyValuePair<string, string>("client_id", Options.ClientId));
body.Add(new KeyValuePair<string, string>("client_secret", Options.ClientSecret));
// Request the token
HttpResponseMessage tokenResponse =
await _httpClient.PostAsync(TokenEndpoint, new FormUrlEncodedContent(body));
tokenResponse.EnsureSuccessStatusCode();
string text = await tokenResponse.Content.ReadAsStringAsync();
// Deserializes the token response
JObject response = JObject.Parse(text);
string accessToken = response.Value<string>("access_token");
string expires = response.Value<string>("expires_in");
string refreshToken = response.Value<string>("refresh_token");
if (string.IsNullOrWhiteSpace(accessToken))
{
_logger.WriteWarning("Access token was not found");
return new AuthenticationTicket(null, properties);
}
// Get the Google user
HttpResponseMessage graphResponse = await _httpClient.GetAsync(
UserInfoEndpoint + Uri.EscapeDataString(accessToken), Request.CallCancelled);
graphResponse.EnsureSuccessStatusCode();
// i will show content of this var later
text = await graphResponse.Content.ReadAsStringAsync();
JObject user = JObject.Parse(text);
//because of permanent exception in GoogleOAuth2AuthenticatedContext constructor i prepare user data with my extension
JObject correctUser = OAuth2Helper.PrepareGoogleUserInfo(user);
// i've replaced this with selfprepared user2
//var context = new GoogleOAuth2AuthenticatedContext(Context, user, accessToken, refreshToken, expires);
var context = new GoogleOAuth2AuthenticatedContext(Context, correctUser, accessToken, refreshToken, expires);
context.Identity = new ClaimsIdentity(
Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if (!string.IsNullOrEmpty(context.Id))
{
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.GivenName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.FamilyName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Name))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String,
Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Email))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String,
Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Profile))
{
context.Identity.AddClaim(new Claim("urn:google:profile", context.Profile, ClaimValueTypes.String,
Options.AuthenticationType));
}
context.Properties = properties;
await Options.Provider.Authenticated(context);
return new AuthenticationTicket(context.Identity, context.Properties);
}
catch (Exception ex)
{
_logger.WriteError("Authentication failed", ex);
return new AuthenticationTicket(null, properties);
}
}
protected override Task ApplyResponseChallengeAsync()
{
// finaly! here it is. i just want to put this two lines here. thats all
Options.ClientId = OAuth2Helper.GetProviderAppId("google");
Options.ClientSecret = OAuth2Helper.GetProviderAppSecret("google");
/* default code ot the method */
}
// no changes
public override async Task<bool> InvokeAsync()
{
/* default code here */
}
// no changes
private async Task<bool> InvokeReplyPathAsync()
{
/* default code here */
}
// no changes
private static void AddQueryString(IDictionary<string, string> queryStrings, AuthenticationProperties properties,
string name, string defaultValue = null)
{
/* default code here */
}
}
After all i get some surprises.
after myhost/signin-google i get
myhost/Account/ExternalLoginCallback?error=access_denied
and 302 redirect back to login page with no success.
that is because of few Exception in internal methods of GoogleOAuth2AuthenticatedContext constructor.
GivenName = TryGetValue(user, "name", "givenName");
FamilyName = TryGetValue(user, "name", "familyName");
and
Email = TryGetFirstValue(user, "emails", "value");
and here is the google response which we translate to JObject user
{
"sub": "XXXXXXXXXXXXXXXXXX",
"name": "John Smith",
"given_name": "John",
"family_name": "Smith",
"profile": "https://plus.google.com/XXXXXXXXXXXXXXXXXX",
"picture": "https://lh5.googleusercontent.com/url-to-the-picture/photo.jpg",
"email": "usermail#domain.com",
"email_verified": true,
"gender": "male",
"locale": "ru",
"hd": "google application website"
}
name is string and TryGetValue(user, "name", "givenName") will fail as TryGetValue(user, "name", "familyName")
emails is missed
thats why i used helper wich translate user to correct correctUser
correctUser is ok but i still have no success. why?
after myhost/signin-google i get
myhost/Account/ExternalLoginCallback
and 302 redirect back to login page with no success.
id in google response is actualy sub so
• Id property of AuthenticatedContext is not filled
• ClaimTypes.NameIdentifier never created
• AccountController.ExternalLoginCallback(string returnUrl) will always redirect us because of loginInfo is null
GetExternalLoginInfo takes AuthenticateResult wich should not be null
and it checks result.Identity for ClaimTypes.NameIdentifier existence
renaming sub into id do the work.
now everything is ok.
it seems that microsoft implementation of katana differs from katana source
because if i use default everything is work without any magic.
if you can correct me, if you know more easiest way to make owin work with AuthenticationOptions determined at runtime based on host name, please tell me
I've recently battled with trying to get multi-tennancy working with the same OAuth provider but with different accounts. I know you wanted to update the options dynamically at runtime but you might not need to do that, hopefully this helps...
I think the reason that you don't have this working, even with overriding all of those classes is because each configured google OAuth account needs to have a unique CallbackPath. This is what determines which registered provider and options will execute on the callback.
Instead of trying to do this dynamically, you can declare each OAuth provider at startup and ensure they have unique AuthenticationType and unique CallbackPath, for example:
//Provider #1
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
{
AuthenticationType = "Google-Site.Com",
ClientId = "abcdef...",
ClientSecret = "zyxwv....",
CallbackPath = new PathString("/sitecom-signin-google")
});
//Provider #2
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
{
AuthenticationType = "Google-AnotherSite.com",
ClientId = "abcdef...",
ClientSecret = "zyxwv....",
CallbackPath = new PathString("/anothersitecom-signin-google")
});
Then where you are calling IOwinContext.Authentication.Challenge you make sure to pass it your correctly named AuthenticationType for the current tenant you want to authenticate. Example: HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google-AnotherSite.com");
The next step is to update your callback path in Google's Developers Console to match your custom callback paths. By default it is "signin-google" but each of these needs to be unique among your declared providers so that the provider knows it needs to handle the specific callback on that path.
I actually just blogged about all of this here in more detail: http://shazwazza.com/post/configuring-aspnet-identity-oauth-login-providers-for-multi-tenancy/

JWT and Web API (JwtAuthForWebAPI?) - Looking For An Example

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();
}

Categories