Using PKCE auth on swagger with an API that accepts bearer auth - .net-core

I'm using Auth0 PKCE authentication on my NuxtJs Application which has a backing Dotnet 6 API. I've set up the API to accept a Bearer token as shown below. For ease of use, I wanted to set up my swagger environment to use PKCE auth also which I've done but the token never authenticates successfully to the Dotnet API. The access token I get from the UI and from postman successfully accessed the API because I am getting the access token and sending it as an authorization header. I attempted to also configure my web API to use OAuth2 along with JWT but the OAuth2 code is never getting hit.
I encountered the same issue originally in postman until I added my API Audience for the token to correctly generate but swagger doesn't seem to allow me to provide an audience.
How do I adjust swagger to use the PKCE auth but generate a token that works for the API?
Swagger setup:
private static void ConfigureSwaggerGen(SwaggerGenOptions options, IConfiguration config)
{
options.OperationFilter<SwaggerDefaultValues>();
options.UseDateOnlyTimeOnlyStringConverters();
var xmlFile = $"{typeof(Program).GetTypeInfo().Assembly.GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
options.DocInclusionPredicate((docName, apiDesc) => apiDesc.RelativePath != null &&
!string.IsNullOrEmpty(apiDesc.GroupName) &&
apiDesc.RelativePath.Contains($"api/{docName}/"));
options.ExampleFilters();
options.TagActionsBy(api => new[]
{api.GroupName});
var oauthAuthority = config[$"{Constants.ConfigSettings}:{Constants.OAuthAuthority}"] ??
config[Constants.OAuthAuthority];
var securitySchema = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
In = ParameterLocation.Header,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl =
new Uri(
$"https://{oauthAuthority}/authorize"),
TokenUrl = new Uri($"https://{oauthAuthority}/oauth/token")
}
},
Description = Constants.SwaggerAuthDescription,
Name = Constants.SwaggerAuthName
};
options.OperationFilter<SecurityRequirementsOperationFilter>();
options.AddSecurityDefinition(Constants.SwaggerAuthName, securitySchema);
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{Id = Constants.SwaggerAuthName, Type = ReferenceType.SecurityScheme}
},
new List<string>()
}
});
}
private static void ConfigureSwaggerUi(SwaggerUIOptions options, IApiVersionDescriptionProvider provider,
IWebHostEnvironment env, IConfiguration config)
{
options.OAuthAppName("TestApp");
options.OAuthClientId(config[$"{Constants.ConfigSettings}:{Constants.OAuthClientId}"] ??
config[Constants.OAuthClientId]);
options.OAuthClientSecret(config[$"{Constants.ConfigSettings}:{Constants.OAuthClientSecret}"] ??
config[Constants.OAuthClientSecret]);
options.OAuthUsePkce();
options.OAuthScopes("profile", "openid", "api");
options.EnablePersistAuthorization();
options.OAuthConfigObject = new OAuthConfigObject
{
ClientId = config[$"{Constants.ConfigSettings}:{Constants.OAuthClientId}"] ??
config[Constants.OAuthClientId],
ClientSecret = config[$"{Constants.ConfigSettings}:{Constants.OAuthClientSecret}"] ??
config[Constants.OAuthClientSecret],
AppName = "TestApp",
Scopes = new List<string> {"profile", "openid", "api"},
AdditionalQueryStringParams = new Dictionary<string, string>
{
{
"audience", config[$"{Constants.ConfigSettings}:{Constants.OAuthAudience}"] ??
config[Constants.OAuthAudience]
}
},
UsePkceWithAuthorizationCodeGrant = true
};
if (env.IsProduction()) options.SupportedSubmitMethods();
foreach (var description in provider.ApiVersionDescriptions)
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
Dotnet Auth Configuration:
private static void ConfigureAuth(IServiceCollection services, IConfiguration config)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
var authority = config[$"{Constants.ConfigSettings}:{Constants.OAuthAuthority}"] ??
config[Constants.OAuthAuthority];
options.Authority = $"https://{authority}";
options.Audience = config[$"{Constants.ConfigSettings}:{Constants.OAuthAudience}"] ??
config[Constants.OAuthAudience];
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = config[$"{Constants.ConfigSettings}:{Constants.OAuthAudience}"] ??
config[Constants.OAuthAudience],
ValidIssuer = $"https://{authority}"
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
if (context.SecurityToken is not JwtSecurityToken token) return Task.CompletedTask;
if (context.Principal?.Identity is ClaimsIdentity identity)
identity.AddClaim(new Claim("access_token", token.RawData));
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
}
};
})
.AddOAuth("Auth0", options =>
{
var authority = config[$"{Constants.ConfigSettings}:{Constants.OAuthAuthority}"] ??
config[Constants.OAuthAuthority];
options.ClientId = config[$"{Constants.ConfigSettings}:{Constants.OAuthClientId}"] ??
config[Constants.OAuthClientId];
options.ClientSecret = config[$"{Constants.ConfigSettings}:{Constants.OAuthClientSecret}"] ??
config[Constants.OAuthClientSecret];
options.CallbackPath = new PathString("/auth/loggedin");
options.AuthorizationEndpoint = $"https://{authority}/authorize";
options.TokenEndpoint = $"https://{authority}/oauth/token";
options.UserInformationEndpoint = $"https://{authority}/userinfo";
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadAsStringAsync();
var jUser = JsonSerializer.SerializeToElement(user);
context.RunClaimActions(jUser);
},
OnRedirectToAuthorizationEndpoint = context =>
{
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
},
OnTicketReceived = context =>
{
return Task.CompletedTask;
},
OnAccessDenied = context =>
{
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
return Task.CompletedTask;
}
};
});
}

Related

Blazor Wasm HttpClient Authorisation header not providing claims

I've upgraded a Blazor Wasm app to .Net 6, by creating new projects and then added MS Identity (not Identity Server) from Chris Sainty's blog. Authorisation looks ok & the user can logon. However the claims are not available to the api controller. This is because it looks like the Authorisation header is not being set for the http client call. If I explicitly set the DefaultRequestHeaders.Authorization to the jwt token then I get the claims in the controller ok. I therefore assume the issue is in how I setup the http client (I thought ApiAuthenticationStateProvider would have done this). Any help would be greatly appreciated.
The relevant parts are:
Client: AuthService:
public async Task<LoginResponse> Login(LoginModel loginModel)
{
loginModel.BaskeyKey = await _localStorageService.GetItemAsync<string>(LocalStorageConstants.BasketKey);
var loginAsJson = JsonSerializer.Serialize(loginModel);
var response = await _httpClient.PostAsync("api/Login", new StringContent(loginAsJson,
Encoding.UTF8, "application/json"));
var loginResult = JsonSerializer.Deserialize<LoginResponse>(
await response.Content.ReadAsStringAsync(),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (!response.IsSuccessStatusCode)
{
return loginResult;
}
await _localStorageService.SetItemAsync("authToken", loginResult.Token);
((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(loginModel.Email);
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("bearer", loginResult.Token);
_basketStateService.SetBasket(loginResult.LoginResponseDto.BasketDto);
return loginResult;
}
Server Startup:
builder.Services.AddDefaultIdentity<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedAccount = true;
})
.AddEntityFrameworkStores<LTDbContext>();
builder.Services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
});
var jwtSettings = builder.Configuration.GetSection("Jwt");
builder.Services.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["JwtIssuer"],
ValidAudience = jwtSettings["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings["JwtSecurityKey"]))
};
options.MapInboundClaims = false;
});
and client setup for httpClient (I need an authorised & unauthorised client - I think)
builder.Services.AddHttpClient(AuthConstants.Auth, client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
});
builder.Services.AddHttpClient(AuthConstants.NoAuth,
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>().CreateClient(AuthConstants.Auth));
and Client - ApiAuthenticationStateProvider
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await _localStorageService.GetItemAsync<string>("authToken");
if (string.IsNullOrWhiteSpace(savedToken))
{
return new AuthenticationState(new ClaimsPrincipal(
new ClaimsIdentity()));
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", savedToken);
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));
}
public void MarkUserAsAuthenticated(string email)
{
var authenticatedUser = new ClaimsPrincipal(
new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, email),
new Claim(ClaimTypes.NameIdentifier, email),
new Claim(ClaimTypes.Email, email)
},
"apiauth"
));
var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
NotifyAuthenticationStateChanged(authState);
}

Adding authentication in api endpoint - ASP.Net Core

I started code own site with backend ASP.NET Core and React. I get a basic code to create an ASP backend with authentication from this
https://jasonwatmore.com/post/2019/10/14/aspnet-core-3-simple-api-for-authentication-registration-and-user-management#running-react
I created an endpoint to my API which allows adding a photo to my server and It's working
[HttpPost("{username}/photos/")]
public IActionResult AddPhoto(String username, [FromForm]IFormFile photo)
{
//add img to static folder
var wwww_root = _webHostEnvironment.WebRootPath;
var path = #"\profiles\" + username + #"\profile_photos\";
var upload_path = wwww_root + path;
var pathForFile = upload_path + photo.FileName;
try
{
using (FileStream fileStream = System.IO.File.Create(pathForFile))
{
photo.CopyTo(fileStream);
fileStream.Flush();
}
}
catch(DirectoryNotFoundException e)
{
return NotFound("This username does not exists");
}
var pathOnServer = "http://" + Request.Host.Value + path + photo.FileName;
var photoImage = _profileService.AddPhoto(username, pathOnServer);
return Ok();
}
but I want to also add authentication to use this endpoint based on the given "username"
i.e. this endpoint returns OK when the Bearer Token is the same as that assigned to the username.
In the given link I found a method configuring the authentication:
public void ConfigureServices(IServiceCollection services)
{
// use sql server db in production and sqlite db in development
if (_env.IsProduction())
services.AddDbContext<DataContext>();
else
services.AddDbContext<DataContext, SqliteDataContext>();
services.AddCors();
services.AddControllers();
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
// configure strongly typed settings objects
var appSettingsSection = _configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var userId = int.Parse(context.Principal.Identity.Name);
var user = userService.GetById(userId);
if (user == null)
{
// return unauthorized if user no longer exists
context.Fail("Unauthorized");
}
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
And api endpoint to Authenticate:
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)
{
var user = _userService.Authenticate(model.Username, model.Password);
if (user == null)
return BadRequest(new { message = "Username or password is incorrect" });
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
// return basic user info and authentication token
return Ok(new
{
Id = user.Id,
Username = user.Username,
FirstName = user.FirstName,
LastName = user.LastName,
Token = tokenString
});
}
but I can't translate this into a solution to my problem.
I am asking for tips on how I can do it
EDIT 1
You can check the claims and token in your upload method
In ConfigureService add this
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
In the upload method use this:
// works if the token is in the header
var tokenFromHeader = HttpContext.Request.Headers["Authorization"];
// or try this if .Net Core is < 3.0
var tokenFromAuth = await HttpContext.Authentication.GetTokenAsync("access_token");
// Get Claims
var identity = HttpContext.User.Identity as ClaimsIdentity;
// Get user id from claims.
var userIdClaim = identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name);
// Use the value.
if (userIdClaim != null && userIdClaim.value != null) {
// your logic here
}
To check the authorization on api call set the attribute [Authorize] on your controller or on your method.
[Authorize]
[HttpPost("{username}/photos/")]
public IActionResult AddPhoto(String username, [FromForm]IFormFile photo)
More info here:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-3.1
The basic code:
Startup.cs -> ConfigureServices
// configure jwt authentication
var jwtSettings = jwtSettingsSection.Get<JwtSettings>();
var key = Encoding.ASCII.GetBytes(jwtSettings.JwtKey);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = jwtSettings.JwtIssuer,
ValidateAudience = true,
ValidAudience = jwtSettings.JwtAudience,
//RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
Startup.cs -> Configure
app.UseAuthentication();
app.UseAuthorization();
Authentication Controller
[AllowAnonymous]
[HttpPost()]
public async Task<IActionResult> AuthenticateUser([FromBody] UserAuthentication userResource)
{
//Your logic of authorization here
//if not authorized
//return Unauthorized();
var tokenString = CreateJwtToken(user);
// return basic user info (without password) and token to store client side
return Ok(new
{
Token = tokenString
});
}
Create JWT Token
public string CreateJwtToken(McUsers user)
{
var key = Encoding.ASCII.GetBytes(jwtSettings.JwtKey);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = jwtSettings.JwtIssuer,
Audience = jwtSettings.JwtAudience,
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
new Claim(ClaimTypes.Role, user.UserRole.Description)
}),
NotBefore = DateTime.UtcNow,
Expires = DateTime.UtcNow.AddMinutes(120),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
return tokenString;
}
Your method in the controller
[Authorize]
[HttpPost("{username}/photos/")]
public IActionResult AddPhoto(String username, [FromForm]IFormFile photo)
{
//add img to static folder
var wwww_root = _webHostEnvironment.WebRootPath;
var path = #"\profiles\" + username + #"\profile_photos\";
var upload_path = wwww_root + path;
var pathForFile = upload_path + photo.FileName;
try
{
using (FileStream fileStream = System.IO.File.Create(pathForFile))
{
photo.CopyTo(fileStream);
fileStream.Flush();
}
}
catch(DirectoryNotFoundException e)
{
return NotFound("This username does not exists");
}
var pathOnServer = "http://" + Request.Host.Value + path + photo.FileName;
var photoImage = _profileService.AddPhoto(username, pathOnServer);
return Ok();
}

Identityserver4 Callback isuue

I'm using identityserver4 with .Net core 3.0 Azure AD authentication for my application.
When user logged in to Microsoft account successfully it should be redirect to angular client application.
Unfortunately I'm getting below error when redirects to the client application.
Below is my OIDC configuration
services.AddAuthentication()
.AddOpenIdConnect("aad", "Demo IdentityServer", options =>
{
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctx =>
{
var message = ctx.ProtocolMessage;
ctx.Properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
}
};
options.CallbackPath = "/signin-oidc";
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SaveTokens = true;
options.Authority = "xxxxxxxxxxxx",
options.ClientId = "xxxxxxxxxxxx",
options.ResponseType = "code id_token";
options.ClientSecret = "L-soqX=3N8kIU0y1gDbB=Ws_QqAMUw#[";
options.GetClaimsFromUserInfoEndpoint = true;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
//,RoleClaimType = "role"
};
});
return services;
This is my call back method.
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
try
{
var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {#claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
{
user = AutoProvisionUser(provider, providerUserId, claims);
}
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username));
await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, localSignInProps, additionalLocalClaims.ToArray());
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
//await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.ClientId));
return Redirect(returnUrl);
}
catch (Exception ex)
{
throw;
}
}
Any help would be appreciate in advance.

.net core 2.2 Cannot retrieve JWT token from Request

I have a asp.net web application using razor pages with PageModel. I need to access a REST web service to get the JWT token
This is my Service configuration within Startup.cs
services.AddIdentity<AppUser, AppUserRole>(cfg =>
{
cfg.User = new UserOptions() { };
cfg.User.RequireUniqueEmail = false;
cfg.SignIn.RequireConfirmedEmail = false;
})
.AddUserManager<AppUserManager<AppUser>>()
.AddUserStore<AppUserStore>()
.AddRoleStore<AppRoleStore>()
.AddDefaultTokenProviders();
services.Configure<TokenOptions>(Configuration.GetSection("TokenConf"));
var tokenConf = Configuration.GetSection("TokenConf").Get<TokenConf>();
services.Configure<AppConf>(Configuration.GetSection("AppConf"));
var appConf = Configuration.GetSection("AppConf").Get<AppConf>();
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidIssuer = tokenConf.Issuer,
ValidAudience = tokenConf.Audience,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(appConf.JWTSecretKey))
};
})
.AddCookie();
services.AddSingleton<IAuthenticationServiceProxy, AuthenticationServiceProxy>();
services.AddHttpContextAccessor();
services.AddMemoryCache();//alternatively we can use services.AddDistributedMemoryCache() and IDistributedCache cache
services.AddSession(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddOptions();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Index");
options.Conventions.AuthorizePage("/Privacy");
options.Conventions.AllowAnonymousToPage("/Account/Login");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
This is my login code within LoginModel : PageModel
public async Task<IActionResult> OnPostAsync(string returnUrl, string handler)
{
if (!ModelState.IsValid)
{
return Page();
}
var appUser = new AppUser() { UserName = UserLogin.Username };
var result = await _signInMgr.PasswordSignInAsync(appUser, UserLogin.Password, false, false);//_signInMgr.PasswordSignInAsync(UserLogin.Username, UserLogin.Password, false, false);
if (result.Succeeded)
{
var userTokenData = _authServPrx.GetTokenData(_appConf.Value.CslLink, UserLogin.Username, UserLogin.Password);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(userTokenData.Token);
var jwt = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
new OkObjectResult(jwt);
}
else
return BadRequest("Bad username or password"); //TODO: better return
return Page();
}
It doesn't work, the redirect doesn't happen, but somehow the authentication cookie does get set. So if I then manually go to /Index
I reach
public void OnGet()
{
var accessToken = Request.Headers["Authorization"];
}
But accessToken is empty.
I just want to redirect and to be able to access my token somehow.
Am I doing something wrong?
That depends on where you want to store the token ,session, cache.... For example, you can pass with query string :
if (result.Succeeded)
{
var userTokenData = _authServPrx.GetTokenData(_appConf.Value.CslLink, UserLogin.Username, UserLogin.Password);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(userTokenData.Token);
var jwt = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
return LocalRedirect(returnUrl+"?token="+jwt);
}
And get token in page :
public void OnGet(string token)
{
}

Core 2.0 API Auth with JWT returns unauthorized

I'm trying to add Token Authentication with JWT to my .Net Core 2.0 app. I have a simple controller that returns a list of users for testing.
[Authorize]
[Route("api/[controller]")]
public class UsersController : Controller
{
...
[HttpGet]
[Route("api/Users/GetUsers")]
public IEnumerable<ApplicationUser> GetUsers()
{
return _userManager.Users;
}
}
I have an API Controller for Token security. It has a login method which returns a Token string result.
[HttpPost(nameof(Login))]
public async Task<IActionResult> Login([FromBody] LoginResource resource)
{
if (resource == null)
return BadRequest("Login resource must be asssigned");
var user = await _userManager.FindByEmailAsync(resource.Email);
if (user == null || (!(await _signInManager.PasswordSignInAsync(user, resource.Password, false, false)).Succeeded))
return BadRequest("Invalid credentials");
string result = GenerateToken(user.UserName, resource.Email);
// Token is created, we can sign out
await _signInManager.SignOutAsync();
return Ok(result);
}
private string GenerateToken(string username, string email)
{
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Email, email),
new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
};
var token = new JwtSecurityToken(
new JwtHeader(new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the secret that needs to be at least 16 characeters long for HmacSha256")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
return new JwtSecurityTokenHandler().WriteToken(token);
}
I have a small console app just for testing the API. When I attempt to Get the Users using the jwt. I receive an immediate "unauthorized". If I remove the "[Authorize]" from the users Controller... success. It appears that my header Authorization is not recognized, but not sure why.
private static async Task<String> GetUsers(String jwt)
{
var url = "https://localhost:44300/";
var apiUrl = $"/api/Users/";
using (var client = new HttpClient() { BaseAddress = new Uri(url) })
{
client.BaseAddress = new Uri(url);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
using (var response = await client.GetAsync(apiUrl))
{
if (response.StatusCode == System.Net.HttpStatusCode.OK)
return await response.Content.ReadAsStringAsync();
else return null;
}
}
}
I'm basing my attempts on the article here ... some of which might be slightly out of date.
Update - Excerpt of Startup.cs
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Jwt";
options.DefaultChallengeScheme = "Jwt";
}).AddJwtBearer("Jwt", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
//ValidAudience = "the audience you want to validate",
ValidateIssuer = false,
//ValidIssuer = "the isser you want to validate",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("the secret that needs to be at least 16 characeters long for HmacSha256")),
ValidateLifetime = true, //validate the expiration and not before values in the token
ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
};
});
Configure...
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
//app.UseJwtBearerAuthentication(
// new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions
// );
}
Solution:
This line was escaping the token therefore causing it to be invalid when passed in the next request:
var result = await response.Content.ReadAsStringAsync();
Replaced with:
var result = await response.Content.ReadAsAsync<string>();
Note: To use this ext method I had to "install-package Microsoft.AspNet.WebApi.Client"
I used JWT authentication in my one of project. I would like to show my implementation, maybe this will help you. But probably you forget to add UseAuthentication(); into configure method in startup class.
startup.cs
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseMvc();
}
public void ConfigureServices(IServiceCollection services)
{
var appSettings = Configuration.GetSection("AppSettings");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}
)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = appSettings["JwtAudience"],
ValidateIssuer = true,
ValidIssuer = appSettings["JwtIssuer"],
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings["JwtSigningKey"]))
};
});
}
generateToken method
private string GenerateToken(string email)
{
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appSettings.Value.JwtSigningKey));
var token = new JwtSecurityToken(
issuer: _appSettings.Value.JwtIssuer,
audience: _appSettings.Value.JwtAudience,
claims: new[]
{
new Claim(JwtRegisteredClaimNames.UniqueName, email),
new Claim(JwtRegisteredClaimNames.Email, email),
new Claim(JwtRegisteredClaimNames.NameId, Guid.NewGuid().ToString())
},
expires: DateTime.Now.AddMinutes(_appSettings.Value.JwtExpireMinute),
signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
I had created a nuget package NetCore.Jwt to simplify this process recently. I didn't find it worth writing all the code each time you needed a Jwt, when you can handle cookies simply with the function SignInAsync. However, if you prefer the manual way, Celal's answer is a clear and straightforward guide for this process.
Alternatively, you can install NetCore.Jwt and use the following in your startup:
services.AddAuthentication(NetCoreJwtDefaults.SchemeName)
.AddNetCoreJwt(options =>
{
// configure your token options such as secret, expiry, and issuer here
});
In your Login function, you can use the extension function for HttpContext
string token = HttpContext.GenerateBearerToken( new Claim[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Email, email),
new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
});
In your program.cs dont forget to have this code (and in order) :
app.UseAuthentication();
app.UseAuthorization();

Resources