I want to know how I can return my generated JWT token to user. I mean I would like this token to be saved e.g. somewhere in the user's browser and sent to my Controller in every query after successfully login.
The token generating functions are already written. Everything works, unfortunately, I do not know how to pass it to the user and then pick it up in other functions / controllers
This is the login controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(LoginDto dto)
{
if (ModelState.IsValid)
{
string token = _accountService.GenerateJwt(dto); //working
if (token != null)
{
// how can I return token there?
return RedirectToAction("LoginSuccess");
}
else
{
ViewBag.error = "Login failed";
return RedirectToAction("LoginFailed");
}
}
}
This is the function that generates the token:
public string GenerateJwt(LoginDto dto)
{
var user = _context.dnozasg2lp_vip_users.FirstOrDefault(u => u.Email == dto.Email);
if (user is null)
{
throw new BadRequestException("Invalid username or password!");
}
var result = _passwordHasher.VerifyHashedPassword(user, user.Password, dto.Password);
if (result == PasswordVerificationResult.Failed)
{
throw new BadRequestException("Invalid username or password!");
}
var claims = new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, user.ID.ToString()),
new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
new Claim(ClaimTypes.Email, $"{user.Email}")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_authenticationSettings.JwtKey));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expiration = DateTime.Now.AddDays(_authenticationSettings.JwtExpireDays);
var token = new JwtSecurityToken(_authenticationSettings.JwtIssuer,
_authenticationSettings.JwtIssuer,
claims,
expires: expiration,
signingCredentials: credentials
);
var tokenHandler = new JwtSecurityTokenHandler();
return tokenHandler.WriteToken(token);
}
And my startup config file:
// JWT Token config below
var authenticationSettings = new AuthenticationSettings();
services.AddSingleton(authenticationSettings);
Configuration.GetSection("Authentication").Bind(authenticationSettings);
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = "Bearer";
option.DefaultScheme = "Bearer";
option.DefaultChallengeScheme = "Bearer";
}).AddJwtBearer(cfg =>{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidIssuer = authenticationSettings.JwtIssuer,
ValidAudience = authenticationSettings.JwtIssuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authenticationSettings.JwtKey)),
};
});
Like I said, the JWT token is successfully generated, but how can I send it to user and then catch it (for example in a "LoginSuccess" action) ?
In a Web API project I am using the following method to sign in my user but at a latter point I want to get my user first name and last name. I am using Web API and .NET 5.
public AuthenticateResponse Authenticate(AuthenticateRequest model, string ipAddress)
{
var user = _userManager.Users
.Where(w => w.UserName == model.Username)
.FirstOrDefault();
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set
lockoutOnFailure: true
var result = _signInManager.PasswordSignInAsync(model.Username, model.Password, true, lockoutOnFailure: false);
User users = new User();
users.Username = model.Username;
users.Password = model.Password;
users.FirstName = user.FirstName;
users.LastName = user.LastName;
if (result.Result.Succeeded)
{
// return null if user not found
if (user == null)
return null;
}
// authentication successful so generate jwt and refresh tokens
var jwtToken = generateJwtToken(users);
var refreshToken = generateRefreshToken(ipAddress);
// save refresh token
// users.RefreshTokens.Add(refreshToken);
return new AuthenticateResponse(users, jwtToken, null);
}
I have this method in a UserService class how would one best access the values from
users.FirstName
users.LastName
from the Web API controller which could be say clubs. As you see I am using the signin manager and usermanager should I simply load an instance of that in my ClubController.
My API method is
[HttpPost]
[Route("Clubs/Create")]
public async Task<IActionResult> Create(ClubViewModelApi clubModel)
{
if (ModelState.IsValid)
{
Club _club = new Club();
_club.Name = clubModel.Name;
_club.Description = clubModel.Description;
_club.isActive = clubModel.isActive;
_club.isDeleted = clubModel.isDeleted;
_club.CreatedDate = DateTime.Now;
_club.CreatedBy = insert first lastname here;
_club.CreatedBy = User.Identity.
_context.Clubs.Add(_club);
await _context.SaveChangesAsync();
return Ok();
}
return View(clubModel);
}
I wish to insert the first name and last name here _club.CreatedBy at this point of the above api end point.
I create my token with this code when I authenticate:
private string generateJwtToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var secret = _configRoot.GetValue<string>("JWTSecret");
_logger.Log(LogLevel.Information, $"JWT Secret from Everleap={secret}");
var key = Encoding.ASCII.GetBytes(secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
My details are stored in a jwttoken. Do I go out to that token again and decrypt it on the controller level.
Response body
{
"id": 0,
"firstName": "david",
"lastName": "buckley",
"username": "davidbuckleyweb#outlook.com",
"jwtToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6IjAiLCJuYmYiOjE2MTY0MzAzNzUsImV4cCI6MTYxNjQzMTI3NSwiaWF0IjoxNjE2NDMwMzc1fQ.P8smaC0PAB5uSrBbI8bbHoc2PKbwIj7mI0jLnBuJz4s",
"refreshToken": null
}
I figured out that what I need to do is extend my claims so that its stored in the token it self I use the following to encode the token
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("CreatedBy", user.FirstName.Substring(0,1).ToUpper() + " " + user.LastName.Substring(0,1).ToUpper()),
new Claim(ClaimTypes.Email, user.Username),
new Claim(ClaimTypes.Name, user.FirstName + " " + user.LastName),
new Claim(ClaimTypes.Role,roles)
}),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
Then to decode it I use the following.
var authorization = Request.Headers[HeaderNames.Authorization];
if (AuthenticationHeaderValue.TryParse(authorization, out var headerValue))
{
// we have a valid AuthenticationHeaderValue that has the following details:
var scheme = headerValue.Scheme;
var JWTToken = headerValue.Parameter;
var token = new JwtSecurityToken(jwtEncodedString: JWTToken);
string name = token.Claims.First(c => c.Type == "CreatedBy").Value;
_club.CreatedBy = name;
}
I am using itfoxtec-identity-saml2 in my Dotnet 3.1 Project. I am initiating request from server and validating the login till here everything is working fine.
After getting response assertion from server and getting claims transformed and creating a session but still my application is unable to login.
Below are snippets of my code for reference.
AuthController.cs
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
try
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
{
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.TransformClaims(claimsPrincipal),isPersistent:true, lifetime: new TimeSpan(1, 0, 0));
var auth = HttpContext.User.Identity.IsAuthenticated;
}
catch (Exception ex)
{
}
return Redirect("~/");
}
ClaimsTransform.cs
public static ClaimsPrincipal TransformClaims(ClaimsPrincipal claimsPrincipal)
{
ClaimsIdentity identity = (ClaimsIdentity)claimsPrincipal.Identity;
var tenantId = identity.FindFirst(ClaimTypes.NameIdentifier);
var Name = identity.FindFirst("firstName");
var firstName = identity.FindFirst("firstName");
var Email = identity.FindFirst("Email");
var UserID = identity.FindFirst("UserID");
var claimsToKeep = new List<Claim> { tenantId, Name,firstName, Email, UserID };
var newIdentity = new ClaimsIdentity(claimsToKeep, identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role);
ClaimsPrincipal newClaims = new ClaimsPrincipal(newIdentity);
return new ClaimsPrincipal(new ClaimsIdentity(claimsToKeep, identity.AuthenticationType, ClaimTypes.Name, ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)claimsPrincipal.Identity).BootstrapContext
});
//return newClaims;
}
After all this my application is redirecting back to login page instead of home page of the application with logged in user.
Help will be appreciated.
You need to set the users identity claim to a claim which exist in the claim set, otherwise the user is not accepted as being authenticated.
If eg. the tenantId claim is the users identity then the users identity claim is ClaimTypes.NameIdentifier in new ClaimsPrincipal(... ClaimTypes.NameIdentifier, ClaimTypes.Role)
ClaimsTransform.cs
public static ClaimsPrincipal TransformClaims(ClaimsPrincipal claimsPrincipal)
{
ClaimsIdentity identity = (ClaimsIdentity)claimsPrincipal.Identity;
var tenantId = identity.FindFirst(ClaimTypes.NameIdentifier);
var Name = identity.FindFirst("firstName");
var firstName = identity.FindFirst("firstName");
var Email = identity.FindFirst("Email");
var UserID = identity.FindFirst("UserID");
var claimsToKeep = new List<Claim> { tenantId, Name,firstName, Email, UserID };
return new ClaimsPrincipal(new ClaimsIdentity(claimsToKeep, identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)claimsPrincipal.Identity).BootstrapContext
});
}
I have a .NET Core 2.0 Web API. I am using Jwt authentication on it.
Whenever the application reaches this line, an exception is thrown:
var userClaims = await _userManager.GetClaimsAsync(user);
The exception is:
System.InvalidOperationException: Sequence contains more than one matching element
What is weird is that this wasn't happening before, it started happening when I upgraded to .NET Core 2.0 from 1.1.
The Seed method for the database is below:
public async Task Seed()
{
var adminUserJ = await _userManager.FindByNameAsync("Ciwan");
var regularUser = await _userManager.FindByNameAsync("John");
if (adminUserJ == null)
{
if (!await _roleManager.RoleExistsAsync("Admin"))
{
var role = new IdentityRole("Admin");
role.Claims.Add(new IdentityRoleClaim<string> { ClaimType = "IsAdmin", ClaimValue = "True" });
await _roleManager.CreateAsync(role);
}
adminUserJ = new BbUser
{
UserName = "Ciwan",
Email = "ck83s9#gmail.com"
};
var userResult = await _userManager.CreateAsync(adminUserJ, "Welcome123");
var roleResult = await _userManager.AddToRoleAsync(adminUserJ, "Admin");
var claimResult = await _userManager.AddClaimAsync(adminUserJ, new Claim("Points", "10"));
if (!userResult.Succeeded || !roleResult.Succeeded || !claimResult.Succeeded)
{
throw new InvalidOperationException("Failed to build user and roles");
}
}
if (regularUser == null)
{
if (!await _roleManager.RoleExistsAsync("Regular"))
{
var role = new IdentityRole("Regular");
role.Claims.Add(new IdentityRoleClaim<string> { ClaimType = "IsRegular", ClaimValue = "True" });
await _roleManager.CreateAsync(role);
}
regularUser = new BbUser
{
UserName = "John",
Email = "j.watson#world.com"
};
var userResult = await _userManager.CreateAsync(regularUser, "BigWow321");
var roleResult = await _userManager.AddToRoleAsync(regularUser, "Regular");
var claimResult = await _userManager.AddClaimAsync(regularUser, new Claim("Points", "10"));
if (!userResult.Succeeded || !roleResult.Succeeded || !claimResult.Succeeded)
{
throw new InvalidOperationException("Failed to build user and roles");
}
}
_context.AddRange(GetListOfArtists(adminUserJ.Id, regularUser.Id));
await _context.SaveChangesAsync();
}
I can't see anything wrong. I tried looking at the AspNetUserClaims table in the database, but all seems OK. I have 2 claims in there, one for each user.
This error happens when I attempt to log in a user, so the request arrives here:
[HttpPost("auth/token")]
public async Task<IActionResult> CreateToken([FromBody] CredentialsDto credentials)
{
try
{
var user = await _userManager.FindByNameAsync(credentials.Username);
if (user != null)
{
if (IsUserPasswordValid(credentials, user))
{
var claims = await GetClaimsAsync(user);
var token = CreateNewJwtToken(claims);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
throw;
}
return BadRequest("Failed to generate token!");
}
private async Task<IEnumerable<Claim>> GetClaimsAsync(BbUser user)
{
var userClaims = await _userManager.GetClaimsAsync(user);
return new[] {
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
}.Union(userClaims);
}
In ASP.NET MVC app, I am trying to implement authentication against external OIDC service. For my testing I am using IdentityServer3 (https://identityserver.github.io/Documentation/) and public OIDC demo server: https://mitreid.org/
I cloned this sample from GitHub: https://github.com/IdentityServer/IdentityServer3.Samples/tree/master/source/MVC%20Authentication
Then added the following code to register the public OIDC server as external login provider:
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "<AuthTypeName>",
Authority = "https://mitreid.org/",
Caption = "MIT Test Server",
ClientId = "<Client Id>",
ClientSecret = "<Client Secret>",
RedirectUri = "https://localhost:44319/", //NOT SURE WHAT TO PUT HERE
ResponseType = "code",
Scope = "openid email profile",
SignInAsAuthenticationType = signInAsType
});
}
The code works, i get the option to login via external OIDC server. The browser redirects to the external server login page and when login and password is entered, the consent page is shown. However, after the browser navigates back to https://localhost:44319/ the user is not authenticated - User.Identity.IsAuthenticated is false.
Question: What should be correct value of RedirectUri property? Does OpenIdConnect middleware have capability to parse the authantication info passed in from external server or it must be coded manually? Is there any sample code how to do this?
I was studying the code and debugging quite a few hours (I am new to this) and I learned that:
This problem is related to OpenIdConnect OWIN Middleware implemented by Microsoft (https://github.com/aspnet/AspNetKatana/tree/dev/src/Microsoft.Owin.Security.OpenIdConnect).
The middleware from Microsoft expect that the OIDC server sends the message using HTTP POST, but the MIT server does HTTP GET
The middleware from Microsoft expect that there is id token along with code in the message obtained from OIDC server, but the MIT server sends only the code.
Looks like the RedirectUri can be any path under /identity because the middleware method AuthenticateCoreAsync() is hit on every request and it does compare request path to configured Options.CallbackPath (which is set from RedirectURI)
So I just had to implement the standard authorization code flow - exchange the code for id token, get claims, create authentication ticket and redirect to IdentityServer /identity/callback endpoint. When I've done this, everything started working. IdentityServer is awesome!
I inherited new set of classes from OpenIdConnect middleware and did override some methods. The key method is async Task<AuthenticationTicket> AuthenticateCoreAsync() in OpenIdConnectAuthenticationHandler. I pasted the code below in case it would help to someone.
public class CustomOidcHandler : OpenIdConnectAuthenticationHandler
{
private const string HandledResponse = "HandledResponse";
private readonly ILogger _logger;
private OpenIdConnectConfiguration _configuration;
public CustomOidcHandler(ILogger logger) : base(logger)
{
_logger = logger;
}
/// <summary>
/// Invoked to process incoming authentication messages.
/// </summary>
/// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns>
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
// Allow login to be constrained to a specific path. Need to make this runtime configurable.
if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path))
return null;
OpenIdConnectMessage openIdConnectMessage = null;
if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
openIdConnectMessage = new OpenIdConnectMessage(Request.Query);
if (openIdConnectMessage == null)
return null;
ExceptionDispatchInfo authFailedEx = null;
try
{
return await CreateAuthenticationTicket(openIdConnectMessage).ConfigureAwait(false);
}
catch (Exception exception)
{
// We can't await inside a catch block, capture and handle outside.
authFailedEx = ExceptionDispatchInfo.Capture(exception);
}
if (authFailedEx != null)
{
_logger.WriteError("Exception occurred while processing message: ", authFailedEx.SourceException);
// Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification.
if (Options.RefreshOnIssuerKeyNotFound && authFailedEx.SourceException.GetType() == typeof(SecurityTokenSignatureKeyNotFoundException))
Options.ConfigurationManager.RequestRefresh();
var authenticationFailedNotification = new AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage,
Exception = authFailedEx.SourceException
};
await Options.Notifications.AuthenticationFailed(authenticationFailedNotification).ConfigureAwait(false);
if (authenticationFailedNotification.HandledResponse)
return GetHandledResponseTicket();
if (authenticationFailedNotification.Skipped)
return null;
authFailedEx.Throw();
}
return null;
}
private async Task<AuthenticationTicket> CreateAuthenticationTicket(OpenIdConnectMessage openIdConnectMessage)
{
var messageReceivedNotification =
new MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage
};
await Options.Notifications.MessageReceived(messageReceivedNotification).ConfigureAwait(false);
if (messageReceivedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (messageReceivedNotification.Skipped)
{
return null;
}
// runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we
// should process.
AuthenticationProperties properties = GetPropertiesFromState(openIdConnectMessage.State);
if (properties == null)
{
_logger.WriteWarning("The state field is missing or invalid.");
return null;
}
// devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users.
if (!string.IsNullOrWhiteSpace(openIdConnectMessage.Error))
{
throw new OpenIdConnectProtocolException(
string.Format(CultureInfo.InvariantCulture,
openIdConnectMessage.Error,
"Exception_OpenIdConnectMessageError", openIdConnectMessage.ErrorDescription ?? string.Empty,
openIdConnectMessage.ErrorUri ?? string.Empty));
}
// tokens.Item1 contains id token
// tokens.Item2 contains access token
Tuple<string, string> tokens = await GetTokens(openIdConnectMessage.Code, Options)
.ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken))
openIdConnectMessage.IdToken = tokens.Item1;
var securityTokenReceivedNotification =
new SecurityTokenReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context,
Options)
{
ProtocolMessage = openIdConnectMessage,
};
await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification).ConfigureAwait(false);
if (securityTokenReceivedNotification.HandledResponse)
return GetHandledResponseTicket();
if (securityTokenReceivedNotification.Skipped)
return null;
if (_configuration == null)
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.Request.CallCancelled)
.ConfigureAwait(false);
// Copy and augment to avoid cross request race conditions for updated configurations.
TokenValidationParameters tvp = Options.TokenValidationParameters.Clone();
IEnumerable<string> issuers = new[] {_configuration.Issuer};
tvp.ValidIssuers = tvp.ValidIssuers?.Concat(issuers) ?? issuers;
tvp.IssuerSigningTokens = tvp.IssuerSigningTokens?.Concat(_configuration.SigningTokens) ?? _configuration.SigningTokens;
SecurityToken validatedToken;
ClaimsPrincipal principal =
Options.SecurityTokenHandlers.ValidateToken(openIdConnectMessage.IdToken, tvp, out validatedToken);
ClaimsIdentity claimsIdentity = principal.Identity as ClaimsIdentity;
var claims = await GetClaims(tokens.Item2).ConfigureAwait(false);
AddClaim(claims, claimsIdentity, "sub", ClaimTypes.NameIdentifier, Options.AuthenticationType);
AddClaim(claims, claimsIdentity, "given_name", ClaimTypes.GivenName);
AddClaim(claims, claimsIdentity, "family_name", ClaimTypes.Surname);
AddClaim(claims, claimsIdentity, "preferred_username", ClaimTypes.Name);
AddClaim(claims, claimsIdentity, "email", ClaimTypes.Email);
// claims principal could have changed claim values, use bits received on wire for validation.
JwtSecurityToken jwt = validatedToken as JwtSecurityToken;
AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, properties);
if (Options.ProtocolValidator.RequireNonce)
{
if (String.IsNullOrWhiteSpace(openIdConnectMessage.Nonce))
openIdConnectMessage.Nonce = jwt.Payload.Nonce;
// deletes the nonce cookie
RetrieveNonce(openIdConnectMessage);
}
// remember 'session_state' and 'check_session_iframe'
if (!string.IsNullOrWhiteSpace(openIdConnectMessage.SessionState))
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.SessionState] = openIdConnectMessage.SessionState;
if (!string.IsNullOrWhiteSpace(_configuration.CheckSessionIframe))
ticket.Properties.Dictionary[OpenIdConnectSessionProperties.CheckSessionIFrame] =
_configuration.CheckSessionIframe;
if (Options.UseTokenLifetime)
{
// Override any session persistence to match the token lifetime.
DateTime issued = jwt.ValidFrom;
if (issued != DateTime.MinValue)
{
ticket.Properties.IssuedUtc = issued.ToUniversalTime();
}
DateTime expires = jwt.ValidTo;
if (expires != DateTime.MinValue)
{
ticket.Properties.ExpiresUtc = expires.ToUniversalTime();
}
ticket.Properties.AllowRefresh = false;
}
var securityTokenValidatedNotification =
new SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context,
Options)
{
AuthenticationTicket = ticket,
ProtocolMessage = openIdConnectMessage,
};
await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification).ConfigureAwait(false);
if (securityTokenValidatedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (securityTokenValidatedNotification.Skipped)
{
return null;
}
// Flow possible changes
ticket = securityTokenValidatedNotification.AuthenticationTicket;
// there is no hash of the code (c_hash) in the jwt obtained from the server
// I don't know how to perform the validation using ProtocolValidator without the hash
// that is why the code below is commented
//var protocolValidationContext = new OpenIdConnectProtocolValidationContext
//{
// AuthorizationCode = openIdConnectMessage.Code,
// Nonce = nonce
//};
//Options.ProtocolValidator.Validate(jwt, protocolValidationContext);
if (openIdConnectMessage.Code != null)
{
var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options)
{
AuthenticationTicket = ticket,
Code = openIdConnectMessage.Code,
JwtSecurityToken = jwt,
ProtocolMessage = openIdConnectMessage,
RedirectUri =
ticket.Properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey)
? ticket.Properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey]
: string.Empty,
};
await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification)
.ConfigureAwait(false);
if (authorizationCodeReceivedNotification.HandledResponse)
{
return GetHandledResponseTicket();
}
if (authorizationCodeReceivedNotification.Skipped)
{
return null;
}
// Flow possible changes
ticket = authorizationCodeReceivedNotification.AuthenticationTicket;
}
return ticket;
}
private static void AddClaim(IEnumerable<Tuple<string, string>> claims, ClaimsIdentity claimsIdentity, string key, string claimType, string issuer = null)
{
string subject = claims
.Where(it => it.Item1 == key)
.Select(x => x.Item2).SingleOrDefault();
if (!string.IsNullOrWhiteSpace(subject))
claimsIdentity.AddClaim(
new System.Security.Claims.Claim(claimType, subject, ClaimValueTypes.String, issuer));
}
private async Task<Tuple<string, string>> GetTokens(string authorizationCode, OpenIdConnectAuthenticationOptions options)
{
// exchange authorization code at authorization server for an access and refresh token
Dictionary<string, string> post = null;
post = new Dictionary<string, string>
{
{"client_id", options.ClientId},
{"client_secret", options.ClientSecret},
{"grant_type", "authorization_code"},
{"code", authorizationCode},
{"redirect_uri", options.RedirectUri}
};
string content;
using (var client = new HttpClient())
{
var postContent = new FormUrlEncodedContent(post);
var response = await client.PostAsync(options.Authority.TrimEnd('/') + "/token", postContent)
.ConfigureAwait(false);
content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
// received tokens from authorization server
var json = JObject.Parse(content);
var accessToken = json["access_token"].ToString();
string idToken = null;
if (json["id_token"] != null)
idToken = json["id_token"].ToString();
return new Tuple<string, string>(idToken, accessToken);
}
private async Task<IEnumerable<Tuple<string, string>>> GetClaims(string accessToken)
{
string userInfoEndpoint = Options.Authority.TrimEnd('/') + "/userinfo";
var userInfoClient = new UserInfoClient(new Uri(userInfoEndpoint), accessToken);
var userInfoResponse = await userInfoClient.GetAsync().ConfigureAwait(false);
var claims = userInfoResponse.Claims;
return claims;
}
private static AuthenticationTicket GetHandledResponseTicket()
{
return new AuthenticationTicket(null, new AuthenticationProperties(new Dictionary<string, string>() { { HandledResponse, "true" } }));
}
private AuthenticationProperties GetPropertiesFromState(string state)
{
// assume a well formed query string: <a=b&>OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey=kasjd;fljasldkjflksdj<&c=d>
int startIndex = 0;
if (string.IsNullOrWhiteSpace(state) || (startIndex = state.IndexOf("OpenIdConnect.AuthenticationProperties", StringComparison.Ordinal)) == -1)
{
return null;
}
int authenticationIndex = startIndex + "OpenIdConnect.AuthenticationProperties".Length;
if (authenticationIndex == -1 || authenticationIndex == state.Length || state[authenticationIndex] != '=')
{
return null;
}
// scan rest of string looking for '&'
authenticationIndex++;
int endIndex = state.Substring(authenticationIndex, state.Length - authenticationIndex).IndexOf("&", StringComparison.Ordinal);
// -1 => no other parameters are after the AuthenticationPropertiesKey
if (endIndex == -1)
{
return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex).Replace('+', ' ')));
}
else
{
return Options.StateDataFormat.Unprotect(Uri.UnescapeDataString(state.Substring(authenticationIndex, endIndex).Replace('+', ' ')));
}
}
}
public static class CustomOidcAuthenticationExtensions
{
/// <summary>
/// Adds the <see cref="OpenIdConnectAuthenticationMiddleware"/> into the OWIN runtime.
/// </summary>
/// <param name="app">The <see cref="IAppBuilder"/> passed to the configuration method</param>
/// <param name="openIdConnectOptions">A <see cref="OpenIdConnectAuthenticationOptions"/> contains settings for obtaining identities using the OpenIdConnect protocol.</param>
/// <returns>The updated <see cref="IAppBuilder"/></returns>
public static IAppBuilder UseCustomOidcAuthentication(this IAppBuilder app, OpenIdConnectAuthenticationOptions openIdConnectOptions)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
if (openIdConnectOptions == null)
throw new ArgumentNullException(nameof(openIdConnectOptions));
return app.Use(typeof(CustomOidcMiddleware), app, openIdConnectOptions);
}
}
and in Startup.cs
public class Startup
{
....
public void Configuration(IAppBuilder app)
{
....
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseCustomOidcAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "<name>",
Authority = "<OIDC server url>",
Caption = "<caption>",
ClientId = "<client id>",
ClientSecret = "<client secret>",
// might be https://localhost:44319/identity/<anything>
RedirectUri = "https://localhost:44319/identity/signin-customoidc",
ResponseType = "code",
Scope = "openid email profile address phone",
SignInAsAuthenticationType = signInAsType
}
);
}
....
}
....
}