ASP.NET core 3 Customized authentication with jwt - asp.net

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

Related

Get Claims in a WebApi Control from a JWT Token

This is my first time attempting to use Asp.Net Core Web Api. I have everything working including authentication and jwt token creation and verification. What I am trying to do is extract the user information that is in the token and use some of it when posting data to the database. I create the token like this:
public string NewToken(string ApiKey, ICAN_Context context)
{
var user = context.TblUserLogins.Where(x => x.ApiKey == ApiKey).FirstOrDefault();
int? CompanyId = context.TblEmployeeCompanies.Where(x => x.EmployeeId == user.EmployeeId).Select(x => x.CompanyId).FirstOrDefault();
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
identity.AddClaim(new Claim("CompanyId", CompanyId.ToString(), ClaimValueTypes.Integer32));
identity.AddClaim(new Claim("EmployeeCompanyId", user.EmployeeCompanyId.ToString(), ClaimValueTypes.Integer32 ));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(identity),
Expires = DateTime.UtcNow.AddMinutes(60),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var jwtString = tokenHandler.WriteToken(token);
return jwtString;
}
I verify the token using a "filter":
public void OnAuthorization(AuthorizationFilterContext context)
{
var tokenManager = (ITokenManager)context.HttpContext.RequestServices.GetService(typeof(ITokenManager));
var result = true;
if(!context.HttpContext.Request.Headers.ContainsKey("Authorization"))
{
result = false;
}
string token = string.Empty;
if(result)
{
token = context.HttpContext.Request.Headers.First(x=>x.Key == "Authorization").Value;
try
{
var claimPrinciple = tokenManager.VerifyToken(token);
}
catch(Exception ex)
{
result = false;
context.ModelState.AddModelError("Unauthorized", ex.ToString());
}
}
if(!result)
{
context.Result = new UnauthorizedObjectResult(context.ModelState);
}
}
I have no problem retrieving the claims info from the token, my question is how do I get the claims from the token in my controller method?
I want to be able to retrieve its values in my methods something like this:
[HttpPost]
[Route("~/api/entity/department")]
public IActionResult CreateDepartment([FromBody] TblCompanyDepartmentsXlu department)
{
var identity = (ClaimsIdentity)User.Identity;
_context.Departments.Add(department);
_context.SaveChanges();
return Ok("Department created successfully!");
}
I also tried this from StackOverflow:
public static ClaimsPrincipal VerifyToken(string jwtToken)
{
TokenManager tokenManager = new TokenManager();
SecurityToken validatedToken;
TokenValidationParameters validationParameters = new TokenValidationParameters();
validationParameters.ValidateLifetime = true;
validationParameters.IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(tokenManager.secretKey);
validationParameters.ValidateAudience = false;
validationParameters.ValidateIssuer = false;
ClaimsPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(jwtToken, validationParameters, out validatedToken);
return principal;
}
[HttpGet]
[Route("~/api/entity/GetEmployees")]
public List<TblEmployees> GetEmployees()
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> claims = identity.Claims;
}
var employees = _context.Employees.ToList();
return employees;
}
but identity.Claims is ALWAYS 0.
I am able to retrieve the claims right after verifying the token:
var claimPrinciple = TokenManager.VerifyToken(token);
I am able to retrieve the claims info here
var claims = claimPrinciple.Identities.First().Claims.ToList();
int? CompanyId = Convert.ToInt32(claims.Where(x => x.Type == "CompanyId").FirstOrDefault().Value);
int EmployeeCompanyId = Convert.ToInt32(claims.Where(x => x.Type == "EmployeeCompanyId").FirstOrDefault().Value);
But I am unable to retrieve them in the controller.
//startup.cs dotnet 6.0 :
builder.Services.AddHttpContextAccessor();
.
.
.
//where you want to use the IHttpContextAccessor :
//For example in user repository :
private readonly IHttpContextAccessor _httpContextAccessor;
public UserRepository(IHttpContextAccessor httpContextAccessor) =>
_httpContextAccessor = httpContextAccessor;
public void LogCurrentUser()
{
var username = _httpContextAccessor.HttpContext.User.Identity.Name;
// ...
}
#Klekmek was correct. All I needed to do was use the built-in JWT token functionality (which I didn't know existed). I removed the AuthenticationFilter and added this to my startup:
To the ConfigureServices section:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt => {
var key = Encoding.ASCII.GetBytes(Configuration["JwtConfig:Secret"]);
jwt.SaveToken = true;
jwt.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ValidateIssuer = false,
ValidateAudience = false
};
});
And the Configure section:
app.UseAuthentication();
After adding that, this worked:
var identity = HttpContext.User.Identity as ClaimsIdentity;

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

Alexa skill and Azure AD authentication

I am trying to build an alexa skill that connects to an enterprise app that uses Azure AD authentication. We set everything up like in this article https://blogs.msdn.microsoft.com/premier_developer/2016/12/12/amazon-alexa-skills-development-with-azure-active-directory-and-asp-net-core-1-0-web-api/, but I am having problems with moving the token from the body of the request to the headers.
The request comes through fine, I can parse it, I add the token to the header but on the test page in amazon I get this message: "There was an error calling the remote endpoint, which returned HTTP 302 : Found"
This is the code for adding the token to the headers:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.Use(async (context, next) =>
{
string path = string.Format(ConfigurationManager.AppSettings["ExchangeTraceDrivePath"], "AlexaRequest", DateTime.Now.ToFileTime(), "");
var stream = context.Request.Body;
try
{
using (var buffer = new MemoryStream())
{
await stream.CopyToAsync(buffer);
var bodyBuffer = new byte[buffer.Length];
buffer.Position = 0L;
buffer.Read(bodyBuffer, 0, bodyBuffer.Length);
var body = Encoding.UTF8.GetString(bodyBuffer);
using (var sw = new StreamWriter(path))
{
sw.WriteLine(DateTime.Now.ToString() + " body: " + body);
sw.WriteLine("---------------------------------------------------------------------------------------------");
foreach (var header in context.Request.Headers)
{
sw.WriteLine(DateTime.Now.ToString() + " header key: " + header.Key);
foreach (var val in header.Value)
{
sw.WriteLine(DateTime.Now.ToString() + " header value: " + val);
}
}
sw.WriteLine("---------------------------------------------------------------------------------------------");
dynamic json = JObject.Parse(body);
sw.WriteLine(DateTime.Now.ToString() + " parsed body: " + json);
sw.WriteLine("---------------------------------------------------------------------------------------------");
if (json?.session?.user?.accessToken != null)
{
sw.WriteLine(DateTime.Now.ToString() + " access accessToken found " +
json?.session?.user?.accessToken);
sw.WriteLine("---------------------------------------------------------------------------------------------");
context.Request.Headers.Add("Authorization",
new string[] { string.Format("Bearer {0}", json?.session?.user?.accessToken) });
foreach (var header in context.Request.Headers)
{
sw.WriteLine(DateTime.Now.ToString() + " header key: " + header.Key);
foreach (var val in header.Value)
{
sw.WriteLine(DateTime.Now.ToString() + " header value: " + val);
}
}
sw.WriteLine("---------------------------------------------------------------------------------------------");
}
buffer.Position = 0L;
context.Request.Body = buffer;
}
}
}
catch
{
}
finally
{
await next.Invoke();
// Restore the original stream.
context.Request.Body = stream;
}
});
//ExpireTimeSpan and SlidinExpiration only work when UseTokenLifetime = false
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// This is NOT ASP.NET Session Timeout (that should be set to same value in web.config)
// This is the expiration on the cookie that holds the Azure AD token
ExpireTimeSpan = TimeSpan.FromMinutes(Convert.ToDouble(expirationTimeSpan)),
// Set SlidingExpiration=true to instruct the middleware to re-issue a new cookie
// with a new expiration time any time it processes a request which is more than
// halfway through the expiration window.
SlidingExpiration = true,
Provider = new CookieAuthenticationProvider
{
// This method is called every time the cookie is authenticated, which
// is every time a request is made to the web app
OnValidateIdentity = CookieAuthNotification.OnValidateIdentity
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
UseTokenLifetime = false,
/*
* Skipping the Home Realm Discovery Page in Azure AD
* http://www.cloudidentity.com/blog/2014/11/17/skipping-the-home-realm-discovery-page-in-azure-ad/
*/
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OpenIdConnectNotification.RedirectToIdentityProvider,
MessageReceived = OpenIdConnectNotification.MessageReceived,
SecurityTokenReceived = OpenIdConnectNotification.SecurityTokenReceived,
SecurityTokenValidated = OpenIdConnectNotification.SecurityTokenValidated,
AuthorizationCodeReceived = OpenIdConnectNotification.AuthorizationCodeReceived,
AuthenticationFailed = OpenIdConnectNotification.AuthenticationFailed
},
});
}
I ended up creating a separate middleware to move the token from the body to the header
public class AlexaJWTMiddleware : OwinMiddleware
{
private readonly OwinMiddleware _next;
public AlexaJWTMiddleware(OwinMiddleware next) : base(next)
{
_next = next;
}
public override Task Invoke(IOwinContext context)
{
var stream = context.Request.Body;
if (context.Request.Headers.ContainsKey("SignatureCertChainUrl")
&& context.Request.Headers["SignatureCertChainUrl"]
.Contains("https://s3.amazonaws.com/echo.api/echo-api-cert-4.pem")
&& !context.Request.Headers.ContainsKey("Authorization"))
{
try
{
using (var buffer = new MemoryStream())
{
stream.CopyToAsync(buffer);
var bodyBuffer = new byte[buffer.Length];
buffer.Position = 0L;
buffer.Read(bodyBuffer, 0, bodyBuffer.Length);
var body = Encoding.UTF8.GetString(bodyBuffer);
dynamic json = JObject.Parse(body);
if (json?.session?.user?.accessToken != null)
{
context.Request.Headers.Add("Authorization",
new string[] { string.Format("Bearer {0}", json?.session?.user?.accessToken) });
}
buffer.Position = 0L;
context.Request.Body = buffer;
}
}
catch
{
}
finally
{
// Restore the original stream.
context.Request.Body = stream;
}
}
else
{
return _next.Invoke(context);
}
return _next.Invoke(context);
}
}
and then adding jwt authentication besides the openId one
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.Use(typeof(AlexaJWTMiddleware));
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = domain,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:AppIdUri"]
},
AuthenticationType = "OAuth2Bearer",
});
//ExpireTimeSpan and SlidinExpiration only work when UseTokenLifetime = false
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// This is NOT ASP.NET Session Timeout (that should be set to same value in web.config)
// This is the expiration on the cookie that holds the Azure AD token
ExpireTimeSpan = TimeSpan.FromMinutes(Convert.ToDouble(expirationTimeSpan)),
// Set SlidingExpiration=true to instruct the middleware to re-issue a new cookie
// with a new expiration time any time it processes a request which is more than
// halfway through the expiration window.
SlidingExpiration = true,
Provider = new CookieAuthenticationProvider
{
// This method is called every time the cookie is authenticated, which
// is every time a request is made to the web app
OnValidateIdentity = CookieAuthNotification.OnValidateIdentity
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
UseTokenLifetime = false,
/*
* Skipping the Home Realm Discovery Page in Azure AD
* http://www.cloudidentity.com/blog/2014/11/17/skipping-the-home-realm-discovery-page-in-azure-ad/
*/
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OpenIdConnectNotification.RedirectToIdentityProvider,
MessageReceived = OpenIdConnectNotification.MessageReceived,
SecurityTokenReceived = OpenIdConnectNotification.SecurityTokenReceived,
SecurityTokenValidated = OpenIdConnectNotification.SecurityTokenValidated,
AuthorizationCodeReceived = OpenIdConnectNotification.AuthorizationCodeReceived,
AuthenticationFailed = OpenIdConnectNotification.AuthenticationFailed
},
});
}

IdentityServer3: Some Claims not being returned from identity server

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

Resources