IdentityServer3: Some Claims not being returned from identity server - asp.net

Context:
I am using ASP.NET MVC with OWIN self host. Below are the rest of the configs/setup.
In my Clients in identity server (notice the AllowedScopes set):
public static class InMemoryClientSource
{
public static List<Client> GetClientList()
{
return new List<Client>()
{
new Client()
{
ClientName = "Admin website",
ClientId = "admin",
Enabled = true,
Flow = Flows.Hybrid,
ClientSecrets = new List<Secret>()
{
new Secret("admin".Sha256())
},
RedirectUris = new List<string>()
{
"https://admin.localhost.com/"
},
PostLogoutRedirectUris = new List<string>()
{
"https://admin.localhost.com/"
},
AllowedScopes = new List<string> {
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
Constants.StandardScopes.Roles
}
}
};
}
}
Here are the Scopes:
public static class InMemoryScopeSource
{
public static List<Scope> GetScopeList()
{
var scopes = new List<Scope>();
scopes.Add(StandardScopes.OpenId);
scopes.Add(StandardScopes.Profile);
scopes.Add(StandardScopes.Email);
scopes.Add(StandardScopes.Roles);
return scopes.ToList();
}
}
In the Identity Server, here's how the server is configured. (Notice the Clients and Scopes are the ones provided above) :
var userService = new UsersService( .... repository passed here .... );
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(InMemoryClientSource.GetClientList())
.UseInMemoryScopes(InMemoryScopeSource.GetScopeList());
factory.UserService = new Registration<IUserService>(resolver => userService);
var options = new IdentityServerOptions()
{
Factory = factory,
SigningCertificate = Certificates.Load(), // certificates blah blah
SiteName = "Identity"
};
app.UseIdentityServer(options);
Finally, on the client web application side, this is how auth is set up:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
Authority = "https://id.localhost.com",
ClientId = "admin",
RedirectUri = "https://admin.localhost.com/",
PostLogoutRedirectUri = "https://admin.localhost.com/",
ResponseType = "code id_token token",
Scope = "openid profile email roles",
ClientSecret = "admin",
SignInAsAuthenticationType = "Cookies"
});
I have implemented a custom class for IUserService:
public class UsersService : UserServiceBase
{
public UsersService( .... repository passed here .... )
{
//.... ctor stuff
}
public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
// var user = .... retrieved from database .....
// ... auth logic ...
if (isAuthenticated)
{
var claims = new List<Claim>();
claims.Add(new Claim(Constants.ClaimTypes.GivenName, user.FirstName));
claims.Add(new Claim(Constants.ClaimTypes.FamilyName, user.LastName));
claims.Add(new Claim(Constants.ClaimTypes.Email, user.EmailAddress));
context.AuthenticateResult = new AuthenticateResult(user.Id.ToString(), user.EmailAddress, claims);
}
return Task.FromResult(0);
}
}
As you see, the claims are passed in this line:
context.AuthenticateResult = new AuthenticateResult(user.Id.ToString(), user.EmailAddress, claims);
When I try logging in to IdentityServer3, I can log in successfully to the client web application. HOWEVER, when I get the user claims, I don't see any identity claims. No given_name, family_name, and email claims. Screenshot below:
Anything I might have missed? Thanks in advance!

My solution was to add a list of claims to my scope configuration in order to return those claims. The wiki's documentation here described it.
For an in-memory client all I did was something like this:
public class Scopes
{
public static IEnumerable<Scope> Get()
{
return new Scope[]
{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.Email,
StandardScopes.Roles,
StandardScopes.OfflineAccess,
new Scope
{
Name = "yourScopeNameHere",
DisplayName = "A Nice Display Name",
Type = ScopeType.Identity,
Emphasize = false,
Claims = new List<ScopeClaim>
{
new ScopeClaim("yourClaimNameHere", true),
new ScopeClaim("anotherClaimNameHere", true)
}
}
};
}
}

Finally found the solution for this problem.
First, I moved the creation of claims to the overridden GetProfileDataAsync (in my UserService class). Here's my implementation of it:
public override Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var identity = new ClaimsIdentity();
UserInfo user = null;
if (!string.IsNullOrEmpty(context.Subject.Identity.Name))
user = _facade.Get(context.Subject.Identity.Name);
else
{
// get the sub claim
var claim = context.Subject.FindFirst(item => item.Type == "sub");
if (claim != null)
{
Guid userId = new Guid(claim.Value);
user = _facade.Get(userId);
}
}
if (user != null)
{
identity.AddClaims(new[]
{
new Claim(Constants.ClaimTypes.PreferredUserName, user.Username),
new Claim(Constants.ClaimTypes.Email, user.EmailAddress)
// .. other claims
});
}
context.IssuedClaims = identity.Claims; //<- MAKE SURE you add the claims here
return Task.FromResult(identity.Claims);
}
Make sure that we pass the claims to the "context.IssueClaims" inside the GetProfileDataAsync() before returning the task.
And for those interested on how my AuthenticateLocalAsync() looks like:
var user = _facade.Get(context.UserName);
if (user == null)
return Task.FromResult(0);
var isPasswordCorrect = BCrypt.Net.BCrypt.Verify(context.Password, user.Password);
if (isPasswordCorrect)
{
context.AuthenticateResult = new AuthenticateResult(user.Id.ToString(), user.Username);
}
return Task.FromResult(0);
I raised a similar issue in IdentityServer3 GitHub project page that contains the explanation on why I encountered my issue. Here's the link:
https://github.com/IdentityServer/IdentityServer3/issues/1938

I am not using the identity server, however I am using the Windows Identity Foundation, which I believe is what IdentityServer uses. In order to access the claims I use:
((ClaimsIdentity)User.Identity).Claims

Related

ASP.NET core 3 Customized authentication with jwt

I am using jwt tokens in my project. When the client logins, he will receive a token with his device id (in mobile) -that was sent to the server in the request body- as the payload...
The thing I want to do is for further requests when the client sends his device id in the body (as a convention in my project) and of course the token in the header request, I want to check if the device id and the token's payload are equal or not.
I did this in a service like this:
public class AuthenticationService
{
public bool IsAuthenticated(HttpRequest req, string deviceId)
{
var token = req.Headers["authorization"];
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken tokenS;
foreach (var stringValue in token)
{
var ss = stringValue.Replace("Bearer ", "");
if (handler.CanReadToken(ss))
{
tokenS = handler.ReadToken(ss) as JwtSecurityToken;
return tokenS.Claims.Any(c => c.Value == deviceId);
}
}
return false;
}
}
And I should inject it in every controller that I want to authorize the user...
if (_authenticationService.IsAuthenticated(Request, deviceId))
{
_logger.LogInformation("authorized!");
}
else
{
_logger.LogCritical("unauthorized");
}
I want to know that if there is a more cleaner way of doing this?
Something to do in the Startup class when I'm configuring the Authentication:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(
Configuration.GetSection("AppSettings:Token").Value)),
ValidateIssuer = false,
ValidateAudience = false,
};
});
AddJwtBearer extension will map the claims in token to user's cliams , you can get the device ID by (if cliam type is deviceId):
var deviceID= context.HttpContext.User.Claims.Where(x => x.Type == "deviceId").FirstOrDefault().Value;
Then you can use Filter or Middleware to read request body and compare the device ID in reuqest body with the one in User's claim . For example , if using Filter :
public class CustomActionFilter : ActionFilterAttribute
{
public override async void OnActionExecuting(ActionExecutingContext context)
{
if (context.HttpContext.User.Identity.IsAuthenticated) {
//read request body
var bodyStr = "";
var req = context.HttpContext.Request;
req.EnableBuffering();
req.Body.Position = 0;
using (var stream = new StreamReader(req.Body))
{
bodyStr = await stream.ReadToEndAsync();
}
// here you get device ID from bodyStr ,for example : deviceIDFromReq
var deviceID= context.HttpContext.User.Claims.Where(x => x.Type == "deviceId").FirstOrDefault().Value;
// then compare
if (deviceIDFromReq.Equals(deviceID))
{
context.Result = new BadRequestObjectResult(new
{
status = "error",
description = "deviceID not matched"
});
}
}
base.OnActionExecuting(context);
}
}
Register as global filter :
services.AddControllers(config =>
{
config.Filters.Add<CustomActionFilter>();
});

How to implement OpenID OAuth2 Server in ASP.NET Framework 4.x Web API?

I'm trying to implement OpenID OAuth 2.0 server with ASP.NET Framework 4.7.2 Web API. It will be used to protect resource APIs with JWT Access/Refresh tokens.
I'm fairly new to OpenID and OAuth so I'm looking for advice/guidelines/libray that I can use to implement this Authorization server.
The Auth server needs to be implemented by using ASP.NET Framework 4.7.2, there is no option for Core at the moment. The resource APIs will be written in ASP.NET Core 2.X.
I've followed the awesome Taiseer's tutorials (Part 1, Part 3, Part 5, and JWT Setup), and currently have OAuth server which can generate JWT Tokens, and Core API which can validate the token.
Here is the code that I currently have.
Auth Server:
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
app.CreatePerOwinContext<SecurityUserManager>(SecurityUserManager.Create);
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth2/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
Provider = new SmAuthorizationServerProvider(),
RefreshTokenProvider = new SmRefreshTokenProvider(),
AccessTokenFormat = new SmJwtFormat("http://localhost:7814"),
ApplicationCanDisplayErrors = true,
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
SmAuthorizationServerProvider.cs
public class SmAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId = string.Empty;
string clientSecret = string.Empty;
Client client = null;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (context.ClientId == null)
{
//Remove the comments from the below line context.SetError, and invalidate context
//if you want to force sending clientId/secrects once obtain access tokens.
context.Validated();
//context.SetError("invalid_clientId", "ClientId should be sent.");
return Task.FromResult<object>(null);
}
using (AuthRepository _repo = new AuthRepository())
{
client = _repo.FindClient(context.ClientId);
}
if (client == null)
{
context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId));
return Task.FromResult<object>(null);
}
if (client.ApplicationType == Models.ApplicationTypes.NativeConfidential)
{
if (string.IsNullOrWhiteSpace(clientSecret))
{
context.SetError("invalid_clientId", "Client secret should be sent.");
return Task.FromResult<object>(null);
}
else
{
if (client.Secret != Helper.GetHash(clientSecret))
{
context.SetError("invalid_clientId", "Client secret is invalid.");
return Task.FromResult<object>(null);
}
}
}
if (!client.Active)
{
context.SetError("invalid_clientId", "Client is inactive.");
return Task.FromResult<object>(null);
}
context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());
context.Validated();
return Task.FromResult<object>(null);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
SecurityUser user = null;
if (allowedOrigin == null) allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
using (AuthRepository _repo = new AuthRepository())
{
user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
string scopes = null;
if (context.Scope.Count > 0)
{
scopes = string.Join(" ", context.Scope.Select(x => x.ToString()).ToArray());
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
// Add the user id as claim here
// Keep the claims number small, the token length increases with each new claim
identity.AddClaim(new Claim("sid", user.Id.ToString()));
// add the client id as claim
if (!string.IsNullOrEmpty(context.ClientId))
{
identity.AddClaim(new Claim("client_id", context.ClientId));
}
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
},
{
"as:scope", (scopes == null) ? string.Empty : scopes
},
{
"userName", context.UserName
}
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId;
// check if the token is created with specified client_id
if (!string.IsNullOrEmpty(currentClient) && !string.IsNullOrEmpty(originalClient))
{
if (originalClient != currentClient)
{
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
return Task.FromResult<object>(null);
}
}
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
// Change auth ticket for refresh token requests if needed
// newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
Client client;
using (AuthRepository _repo = new AuthRepository())
{
client = _repo.FindClient(context.ClientId);
}
var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
oAuthIdentity.AddClaim(new Claim("client_id", client.Id));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
}
});
var ticket = new AuthenticationTicket(oAuthIdentity, props);
context.Validated(ticket);
return base.GrantClientCredentials(context);
}
}
SmJwtFormat.cs
public class SmJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private const string AudiencePropertyKey = "as:scope";
private readonly string _issuer = string.Empty;
private AuthRepository authRepo;
private SecurityKey signingKey;
private string secret = "P#ssw0rd-7BBF8546-C8C1-44D9-A404-9E1CAF80EC9D-F2FEC38D-2041-499E-9FAA-218C8B1EEC7B";
public SmJwtFormat(string issuer)
{
_issuer = issuer;
authRepo = new AuthRepository();
// Generating the signingKey
string symmetricKeyAsBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
signingKey = new SymmetricSecurityKey(keyByteArray);
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
// The token audience from the JWT terminology is the same as the token Scope in OAuth terminology.
string scope = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ? data.Properties.Dictionary[AudiencePropertyKey] : null;
if (string.IsNullOrWhiteSpace(scope)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience/scope");
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
if (scope != null)
{
var scopesList = scope.Split(' ').ToList();
var audClaims = scopesList.Select(s => new Claim("aud", s));
data.Identity.AddClaims(audClaims);
}
var token = new JwtSecurityToken(_issuer, null, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingCredentials);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.WriteToken(token);
return jwt;
}
public AuthenticationTicket Unprotect(string protectedText)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = _issuer,
IssuerSigningKey = signingKey,
};
var handler = new JwtSecurityTokenHandler();
SecurityToken token = null;
// Unpack token
var pt = handler.ReadJwtToken(protectedText);
string t = pt.RawData;
var principal = handler.ValidateToken(t, tokenValidationParameters, out token);
var identity = principal.Identities;
return new AuthenticationTicket(identity.First(), new AuthenticationProperties());
}
}
Here is my resource ASP.NET Core 2.2 API Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddAuthorization();
// identity http://localhost:7814
// resource https://localhost:44337
var key = "P#ssw0rd-7BBF8546-C8C1-44D9-A404-9E1CAF80EC9D-F2FEC38D-2041-499E-9FAA-218C8B1EEC7B";
string symmetricKeyAsBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(key));
var keyByteArray = Convert.FromBase64String(symmetricKeyAsBase64);
var securityKey = new SymmetricSecurityKey(keyByteArray);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = "http://localhost:7814/";
o.RequireHttpsMetadata = false;
o.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = "http://localhost:7814",
ValidateAudience = true,
ValidAudiences = new List<string>()
{
"api1"
},
//IssuerSigningKey = securityKey
};
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
}
Currently I'm able to generate tokens by posting to http://localhost:7814/oauth2/token with the following parameters:
grant_type=password
username=user1
password=p#ssw0rd
client_id=smLocalhost
client_secret=secret
scope=api1 api2
After that the token can be used to access secured endpoints in the resource API.
I've researched IdentityServer4, OpenIdDict and AspNet.Security.OpenIdConnect.Server, but they seems to be working only with ASP.NET Core.
So after all this stuff, my questions are:
1. How to add OpenID on top of that?
2. Is there a library that I can use?
3. Could you please give me tutorial/advice what can I do to implement
it?
4. How to implement discovery document endpoint (.well-known/openid-configuration) and rotate the public keys for asymmetric token signing?
Thanks in advance!

ASP.NET API OAuth2 Refresh token - Deserialize ticket not working

I'm implementing the OAuth 2 refresh token with ASP.NET API2 and OWIN, the following code is my OAuthAuthorizationOptions
public static OAuthAuthorizationServerOptions AuthorizationServerOptions
{
get
{
if (_AuthorizationServerOptions == null)
{
_AuthorizationServerOptions = new OAuthAuthorizationServerOptions()
{
AuthenticationType = OAuthDefaults.AuthenticationType,
AllowInsecureHttp = true,
TokenEndpointPath = new PathString(AuthSettings.TokenEndpoint),
AuthorizeEndpointPath = new PathString(AuthSettings.AuthorizeEndpoint),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(AuthSettings.TokenExpiry),
Provider = new CustomOAuthAuthorizationServerProvider(AuthSettings.PublicClientId),
// TODO: Remove the dependency with Thinktecture.IdentityModel library here
AccessTokenFormat = new CustomJWTFormat(),
RefreshTokenProvider = new CustomRefreshTokenProvider()
};
}
return _AuthorizationServerOptions;
}
}
Here is my CustomRefreshTokenProvider class
public override Task CreateAsync(AuthenticationTokenCreateContext context)
{
var identifier = context.Ticket.Identity.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
if (identifier == null || string.IsNullOrEmpty(identifier.Value))
{
return Task.FromResult<object>(null);
}
var refreshToken = HashHelper.Hash(Guid.NewGuid().ToString("n"));
var tokenIssued = DateTime.UtcNow;
var tokenExpire = DateTime.UtcNow.AddSeconds(AuthSettings.RefreshTokenExpiry);
context.Ticket.Properties.IssuedUtc = tokenIssued;
context.Ticket.Properties.ExpiresUtc = tokenExpire;
context.Ticket.Properties.AllowRefresh = true;
var protectedTicket = context.SerializeTicket();
AuthService.AddUserRefreshTokenSession(
identifier.Value,
refreshToken,
tokenIssued,
tokenExpire,
protectedTicket);
context.SetToken(refreshToken);
return Task.FromResult<object>(null);
}
public override Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var refToken = context.Token;
var protectedTicket = AuthService.GetProtectedTicket(refToken);
if (!string.IsNullOrEmpty(protectedTicket))
{
context.DeserializeTicket(protectedTicket);
}
return Task.FromResult<object>(null);
}
I have used postman to send POST request to the token endpoint as below
Postman refresh token
the server return 400 bad request status code.
I debugged and found that the context.DeserializeTicket(protectedTicket)
throw an Exception
Exception thrown: 'System.Security.Cryptography.CryptographicException' in System.Web.dll
I don't think it is the expiration issue because
the AuthSettings.RefreshTokenExpiry is 30 days from now.
I also tried to add Machine key to my web.config OAuth Refresh Token does not deserialize / invalid_grant
but it still not working.
Does anyone have an idea?
Any solutions will be highly appreciated.
Sorry for the late answered.
I have resolved this issue and end up with a solution that remove entirely Thinktecture.Identity out of my project, since it's somehow conflict with System.IdentityModel.Tokens.JWT.dll
Another solution if you still want to use Thinktecture.Identity is that downgrade System.IdentityModel.Tokens.JWT to v4.0.2.206221351 (this version worked for me, I haven't tested with another version).
Here is the code of CustomJWTFormat.cs
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using MST.Service.Core.Logging;
using System;
using System.Security.Claims;
namespace MST.Service.Core.Auth
{
public class CustomJWTFormat : ISecureDataFormat
{
private byte[] _SymetricKey = null;
public CustomJWTFormat()
{
_SymetricKey = Convert.FromBase64String(AuthSettings.JwtTokenSecret);
}
///
/// Create jwt token
///
///
/// Token string
public string Protect(AuthenticationTicket data)
{
var tokenHandler = new System.IdentityModel.Tokens.JwtSecurityTokenHandler();
var now = DateTime.UtcNow;
System.IdentityModel.Tokens.JwtSecurityToken jwtSecurityToken = new System.IdentityModel.Tokens.JwtSecurityToken(
AuthSettings.Issuer,
AuthSettings.JwtAudiences,
data.Identity.Claims,
DateTime.UtcNow,
DateTime.UtcNow.AddMinutes(AuthSettings.TokenExpiry),
new System.IdentityModel.Tokens.SigningCredentials(
new System.IdentityModel.Tokens.InMemorySymmetricSecurityKey(_SymetricKey),
System.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature,
System.IdentityModel.Tokens.SecurityAlgorithms.Sha256Digest)
);
var token = tokenHandler.WriteToken(jwtSecurityToken);
return token;
}
public AuthenticationTicket Unprotect(string protectedText)
{
var tokenHandler = new System.IdentityModel.Tokens.JwtSecurityTokenHandler();
var validationParameters = new System.IdentityModel.Tokens.TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = true,
ValidateLifetime = true,
AuthenticationType = OAuthDefaults.AuthenticationType,
ValidIssuers = new string[] { AuthSettings.Issuer },
ValidAudiences = new string[] { AuthSettings.JwtAudiences },
ValidateAudience = true,
ValidateIssuerSigningKey = false,
IssuerSigningKey = new System.IdentityModel.Tokens.InMemorySymmetricSecurityKey(_SymetricKey)
};
System.IdentityModel.Tokens.SecurityToken securityToken = null;
ClaimsPrincipal principal = null;
try
{
principal = tokenHandler.ValidateToken(protectedText, validationParameters, out securityToken);
var validJwt = securityToken as System.IdentityModel.Tokens.JwtSecurityToken;
if (validJwt == null)
{
throw new ArgumentException("Invalid JWT");
}
}
catch (Exception ex)
{
LoggerManager.AuthLog.Error($"Parse token error: {ex.ToString()}");
return null;
}
// Validation passed. Return a valid AuthenticationTicket:
return new AuthenticationTicket(principal.Identity as ClaimsIdentity, new AuthenticationProperties());
}
}
}
and JWTBearerAuthenticationOptions (if you host Resource Server and Authorization Server in the same machine)
public static JwtBearerAuthenticationOptions JwtAuthenticationOptions
{
get
{
if (_JwtAuthenticationOptions == null)
{
_JwtAuthenticationOptions = new JwtBearerAuthenticationOptions()
{
//AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
AuthenticationType = OAuthDefaults.AuthenticationType,
AllowedAudiences = new[] { AuthSettings.JwtAudiences },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(AuthSettings.Issuer, AuthSettings.JwtTokenSecret)
}
};
}
return _JwtAuthenticationOptions;
}
}
in Startup.cs just register middleware as usual
app.UseJwtBearerAuthentication(AuthenticationOptions.JwtAuthenticationOptions);

OWIN - signing and encrypting requests

In our asp.net MVC5 web site we're authenticating against multiple ADFS servers. One of these requests that we sign (and preferably encrypt) our request.
We're using OWIN and the UseWsFederationAuthentication extension method to set up the options for each ADFS server (see below).
var adfsLoginProviderOptions = new WsFederationAuthenticationOptions
{
MetadataAddress = adfsLoginProvider.MetadataUrl,
Wtrealm = AppSettings.FirstAgendaWtRealm,
AuthenticationMode = AuthenticationMode.Passive,
AuthenticationType = adfsLoginProvider.Name,
CallbackPath = new PathString("/adfs/callback"),
UseTokenLifetime = true
};
app.UseWsFederationAuthentication(adfsLoginProviderOptions);
My problem is, I don't see a obvious option to set up request signing and encryption and I can't seem to find anyone else who has done this.
I did some research and found the following.
I needed to register to SecurityTokenHandlers:
One for decrypting the encrypted token
One for validating the signed token
They are registered as follows:
var adfsLoginProviderOptions = new WsFederationAuthenticationOptions
{
MetadataAddress = adfsLoginProvider.MetadataUrl,
Wtrealm = "http://[your-realm]",
AuthenticationMode = AuthenticationMode.Passive,
AuthenticationType = adfsLoginProvider.Name,
UseTokenLifetime = false,
CallbackPath = new PathString("/adfs/callback/" + adfsLoginProvider.ID.ToString()),
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = adfsLoginProvider.Name
},
SecurityTokenHandlers = new SecurityTokenHandlerCollection
{
new EncryptedSecurityTokenHandler(new X509CertificateStoreTokenResolver(StoreName.My,StoreLocation.LocalMachine)),
new SamlSecurityTokenHandler
{
CertificateValidator = X509CertificateValidator.None,
Configuration = new SecurityTokenHandlerConfiguration()
{
AudienceRestriction = audienceRestriction,
IssuerNameRegistry = issuerRegistry
}
}
},
};
The EncryptedSecurityTokenHandler is implemented as follows:
public class EncryptedSecurityTokenHandler : System.IdentityModel.Tokens.EncryptedSecurityTokenHandler, ISecurityTokenValidator
{
public EncryptedSecurityTokenHandler(SecurityTokenResolver securityTokenResolver)
{
Configuration = new SecurityTokenHandlerConfiguration
{
ServiceTokenResolver = securityTokenResolver
};
}
public override bool CanReadToken(string securityToken)
{
return base.CanReadToken(new XmlTextReader(new StringReader(securityToken)));
}
public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
// Read token will decrypt it and look for another SecurityTokenHandler in the same collection to do the actual validation
validatedToken = ReadToken(new XmlTextReader(new StringReader(securityToken)), Configuration.ServiceTokenResolver);
if (ContainingCollection != null)
{
var identities = ContainingCollection.ValidateToken(validatedToken);
var principal = new ClaimsPrincipal(identities.First());
return principal;
}
return new ClaimsPrincipal(base.ValidateToken(validatedToken));
}
public int MaximumTokenSizeInBytes { get; set; }
}
And the SamlSecurityTokenHandler:
public class SamlSecurityTokenHandler : System.IdentityModel.Tokens.SamlSecurityTokenHandler, ISecurityTokenValidator
{
public override bool CanReadToken(string securityToken)
{
return base.CanReadToken(XmlReader.Create(new StringReader(securityToken)));
}
public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters,
out SecurityToken validatedToken)
{
validatedToken = ReadToken(new XmlTextReader(new StringReader(securityToken)), Configuration.ServiceTokenResolver);
var identities = ValidateToken(validatedToken);
var newIdentities = identities.Select(d => new ClaimsIdentity(d.Claims, "ExternalCookie"));
var claimsPrincipal = new ClaimsPrincipal(newIdentities);
return claimsPrincipal; ;
}
public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SecurityToken token)
{
var identities = base.ValidateToken(token);
return identities
}
public int MaximumTokenSizeInBytes { get; set; }
}
Audience restriction is the realm of the application:
var audienceRestriction = new AudienceRestriction(AudienceUriMode.Always);
audienceRestriction.AllowedAudienceUris.Add(new Uri(http://[your-realm]));
IssuerRegistry is the registry of the issuer signing certificates:
var issuerRegistry = new ConfigurationBasedIssuerNameRegistry();
issuerRegistry.AddTrustedIssuer(adfsLoginProvider.SigningCertThumbprint, adfsLoginProvider.Issuer);

Pass a variable from a Custom Filter to controller action method

I have a Web Api project.
I have implemented a custom Authentication Attribute like so:
public class TokenAuthenticationAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
// In auth web method you should implement functionality of authentication
// so that client app could be able to get token
if (actionContext.Request.RequestUri.AbsolutePath.Contains("api/auth/login"))
{
return;
}
// Receive token from the client. Here is the example when token is in header:
var token = HttpContext.Current.Request.Headers["Token"];
// Put your secret key into the configuration
var secretKey = ConfigurationManager.AppSettings["JWTSecurityKey"];
try
{
string jsonPayload = JWT.JsonWebToken.Decode(token, secretKey);
int separatorIndex = jsonPayload.IndexOf(';');
string userId = "";
DateTime timeIssued = DateTime.MinValue;
if (separatorIndex >= 0)
{
//userId = UTF8Encoding.UTF8.GetString(Convert.FromBase64String(jsonPayload.Substring(0, separatorIndex)));
userId = jsonPayload.Substring(0, separatorIndex);
timeIssued = DateTime.Parse(jsonPayload.Substring(separatorIndex + 1));
}
short TokenTTL = 10;
//try{
//Int16.TryParse(ConfigurationManager.AppSettings["TokenTTL"],TokenTTL);
//}catch(Exception e){ //}
if ((DateTime.Now.Subtract(timeIssued).TotalMinutes >= TokenTTL))
{
throw new HttpResponseException(HttpStatusCode.Forbidden);
}
//Save user in context
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, userId)
};
var id = new ClaimsIdentity(claims, "Basic");
var principal = new ClaimsPrincipal(new[] { id });
actionContext.Request.GetRequestContext().Principal = principal;
}
catch (JWT.SignatureVerificationException)
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
}
}
Now how do I get hold of that user in my actionmethod?
[BasicHttpAuthorizeAttribute]
[httpGet]
public void Login()
{
// how do i get user here
}
/////// Save the string username to the context so that I can acess
it in the controler.
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, "john")
};
var id = new ClaimsIdentity(claims, "Basic");
var principal = new ClaimsPrincipal(new[] { id });
actionContext.Request.GetRequestContext().Principal = principal;
// how do i get user here
var name = User.Identity.Name;
BTW, use an authentication filter instead of an authorization filter to perform authentication. See my blog post - http://lbadri.wordpress.com/2014/02/13/basic-authentication-with-asp-net-web-api-using-authentication-filter/.

Resources