I have working example of JWT Token. It is work good and when I put this token to storage in angularJS I can go to api controller with attribute [Authorize]. But when I generate token with role, I cant go to attribute [Authorize(Roles = "Admin")]. As I know I role save in token and I need`t to change a header of request to api. My code below
public class AuthOptions
{
public const string ISSUER = "MyAuthServer";
public const string AUDIENCE = "http://localhost:51489/";
const string KEY = "mysupersecret_secretkey!123";
public const int LIFETIME = 60;
public static SymmetricSecurityKey GetSymmetricSecurityKey()
{
return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY));
}
}
[HttpPost]
[AllowAnonymous]
[Route("login")]
public async Task Login([FromBody]LoginViewModel model)
{
var identity = await GetIdentity(model.Email, model.Password);
if (identity == null)
{
Response.StatusCode = 400;
await Response.WriteAsync("Invalid username or password.");
return;
}
var now = DateTime.UtcNow;
var jwt = new JwtSecurityToken(
issuer: AuthOptions.ISSUER,
audience: AuthOptions.AUDIENCE,
notBefore: now,
claims: identity.Claims,
expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)),
signingCredentials: new
SigningCredentials(AuthOptions.GetSymmetricSecurityKey(),
SecurityAlgorithms.HmacSha256));
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new
{
access_token = encodedJwt,
username = identity.Name,
};
Response.ContentType = "application/json";
await Response.WriteAsync(JsonConvert.SerializeObject(response, new
JsonSerializerSettings { Formatting = Formatting.Indented }));
return;
}
private async Task<ClaimsIdentity> GetIdentity(string username, string
password)
{
var user = _db.User.FirstOrDefault(x => x.Email == username);
if (user != null)
{
var checkPass = _userManager.CheckPasswordAsync(user, password);
if (!checkPass.Result)
return null;
var userRoles = await _userManager.GetRolesAsync(user);
string role = userRoles[0];
var claims = new List<Claim>
{
new Claim(ClaimsIdentity.DefaultNameClaimType, user.Email),
new Claim(ClaimsIdentity.DefaultRoleClaimType, role)
};
ClaimsIdentity claimsIdentity =
new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
return claimsIdentity;
}
return null;
}
Startup
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters =
newTokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = AuthOptions.ISSUER,
ValidateAudience = true,
ValidAudience = AuthOptions.AUDIENCE,
ValidateLifetime = true,
IssuerSigningKey =AuthOptions.GetSymmetricSecurityKey(),
ValidateIssuerSigningKey = true,
};
});
Put to storage with angularJS $cookies
$http.defaults.headers.common['Authorization'] = 'Bearer ' +
response.data.access_token;
With this atribute is working
[Authorize]
With this atribute not working
[Authorize(Roles = "Admin")]
You are storing your Role as a claim in the token.
You will need to create a policy that works of the role claim that you have assigned to your token.
Create a policy in your Startup.cs
services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy => policy.RequireClaim("Role", "Admin"));
});
Then you can use this authorization attribute [Authorize(Policy = "Admin")]
Related
I'm trying to make an endpoint for logging in with a token in ASP.NET Core. The API is using EFCore, but my code is taking a long time to log me in.
This is the service:
public async Task<AuthModel> GetTokenAsync(UserLoginDto model)
{
var authModel = new AuthModel();
var user = await _userManager.FindByEmailAsync(model.Email);
if (user is null || !await _userManager.CheckPasswordAsync(user, model.Password))
{
authModel.Message = "Email or Password is incorrect!";
return authModel;
}
var jwtSecurityToken = await CreateJwtToken(user);
var rolesList = await _userManager.GetRolesAsync(user);
authModel.IsAuthenticated = true;
authModel.Token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
authModel.Email = user.Email;
authModel.Username = user.UserName;
//authModel.ExpiresOn = jwtSecurityToken.ValidTo;
authModel.Roles = rolesList.ToList();
if (user.RefreshTokens.Any(t => t.IsActive))
{
var activeRefreshToken = user.RefreshTokens.FirstOrDefault(t => t.IsActive);
authModel.RefreshToken = activeRefreshToken.Token;
authModel.RefreshTokenExpiration = activeRefreshToken.ExpiresOn;
}
else
{
var refreshToken = GetRefreshToken();
authModel.RefreshToken = refreshToken.Token;
authModel.RefreshTokenExpiration = refreshToken.ExpiresOn;
user.RefreshTokens.Add(refreshToken);
await _userManager.UpdateAsync(user);
}
return authModel;
}
Endpoint:
[HttpPost]
public async Task<ActionResult> Login([FromBody]UserLoginDto loginDto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var result = await _authService.GetTokenAsync(loginDto);
if (!string.IsNullOrEmpty(result.RefreshToken))
{
_authService.SetRefreshTokenInCookie(result.RefreshToken, result.RefreshTokenExpiration);
}
return Ok(result);
}
I've been trying to get my authentication and authorization process working with google for two weeks now using react on the frontend and asp.net core on the backend.
Incredible as it may seem, I can't find a guide that addresses all this development context despite being something very basic.
Anyway, the best I could get was this code below, but I don't know how instead of returning the View, return my frontend in React.
And also how to do the process in react.
I know the question may sound a little generic, but I'm really lost.
This is my controller
public IActionResult GoogleLogin()
{
string redirectUrl = Url.Action("GoogleResponse", "Account");
var properties = signInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl);
return new ChallengeResult("Google", properties);
}
[AllowAnonymous]
public async Task<IActionResult> GoogleResponse()
{
ExternalLoginInfo info = await signInManager.GetExternalLoginInfoAsync();
if (info == null)
return RedirectToAction(nameof(Login));
var result = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, false);
string[] userInfo = { info.Principal.FindFirst(ClaimTypes.Name).Value, info.Principal.FindFirst(ClaimTypes.Email).Value };
if (result.Succeeded)
return View(userInfo);
else
{
AppUser user = new AppUser
{
Email = info.Principal.FindFirst(ClaimTypes.Email).Value,
UserName = info.Principal.FindFirst(ClaimTypes.Email).Value
};
IdentityResult identResult = await userManager.CreateAsync(user);
if (identResult.Succeeded)
{
identResult = await userManager.AddLoginAsync(user, info);
if (identResult.Succeeded)
{
await signInManager.SignInAsync(user, false);
return View(userInfo);
}
}
return AccessDenied();
}
}`
This is my Startup
`services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddAuthentication()
.AddGoogle(opts =>
{
opts.ClientId = "715561755180-h72ug8g5v4sfgcn150n8mjaq75oacmp8.apps.googleusercontent.com";
opts.ClientSecret = "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";
opts.SignInScheme = IdentityConstants.ExternalScheme;
});`
I have a regular jwt authentication with email and password works well with react,.net core and identity, but google login is my problem
I expect a guide or tips to help me move forward with the project
I have the following flow Asp.Net Core App (App) that calls Asp.Net Core Web API (API1) that inturn calls another Asp.Net Web API (API2)
I'm using IdentityServer4 with Windows Authentication to Authenticate my users
here is my code:
ApiResources definition:
new ApiResource("api1", "api1", new List<string> { JwtClaimTypes.Name, JwtClaimTypes.Email}),
new ApiResource("api2", "api2", new List<string> { JwtClaimTypes.Name, JwtClaimTypes.Email})
Clients definition
new Client
{
ClientId = "app1",
ClientName = "app1",
ClientSecrets = { new Secret("app1".Sha256())},
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
IdentityServerConstants.StandardScopes.Email,
"api1"
},
RedirectUris = { "https://localhost:44375/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:44375/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:44375/signout-callback-oidc" },
AllowOfflineAccess = true,
RequireConsent = false,
AccessTokenLifetime = 5
},
new Client
{
ClientId = "api1",
ClientSecrets = { new Secret("api1".Sha256())},
AllowedGrantTypes = {"delegation" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api2"}
}
Delegation code in API1
public async Task<string> DelegateAsync(string userToken)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:44382/");
if (disco.IsError) throw new Exception(disco.Error);
var tokenResponse = await client.RequestTokenAsync(new TokenRequest()
{
Address = disco.TokenEndpoint,
GrantType = "delegation",
ClientId = "api1",
ClientSecret = "api1",
Parameters =
{
{"scope" , "api2 email profile openid" },
{"token", userToken }
}
});
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
_logger.LogInformation($"new: {tokenResponse.AccessToken}");
return tokenResponse.AccessToken;
}
IdentityServer4 DelegationGrantValidator:
public class DelegationGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _validator;
public DelegationGrantValidator(ITokenValidator validator)
{
_validator = validator;
}
public string GrantType => "delegation";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("token");
if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken);
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// get user's identity
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType);
return;
}
}
in API1 User.Identity.Name = "Domain\UserName"
but in API2 User.Identity.Name = null
is there anything missing that I should do solve this issue?
P.S.: if I call the IdentityServer UserInfo endpoint from API2 I'll get the expected UserName
After a lot of search I have finally found the answer.
I've followed what Rory Braybrook described in this Article and everything is working now
I'm getting a "You do not have permission to view this directory or page." error when I try to LoginAsync with an access token and MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory. This works with the equivalent form with MobileServiceAuthenticationProvider.MicrosoftAccount. I'm not sure why this isn't working. Is there a configuration I'm missing?
var msaProvider = await WebAuthenticationCoreManager.FindAccountProviderAsync(
"https://login.microsoft.com",
"https://login.microsoftonline.com/3dd13bb9-5d0d-dd2e-9d1e-7a966131bf85");
string clientId = "6d15468d-9dbe-4270-8d06-a540dab3252f";
WebTokenRequest request1 = new WebTokenRequest(msaProvider, "User.Read", clientId);
request1.Properties.Add("resource", "https://graph.microsoft.com");
WebTokenRequestResult result =
await WebAuthenticationCoreManager.RequestTokenAsync(request1);
if (result.ResponseStatus == WebTokenRequestStatus.Success)
{
var token = result.ResponseData[0].Token;
var token1 = new JObject
{
{ "access_token", token }
};
var user = await App.mobileServiceClient.LoginAsync(
MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, token1);
I was able to get MSAL.NET to work for this per code below. The key is the { resourceId + "/user_impersonation" } scope.
PublicClientApplication pca = new PublicClientApplication(clientId)
{
RedirectUri = redirectUri
};
string[] scopes = { resourceId + "/user_impersonation" };
var users = await pca.GetAccountsAsync();
var user = users.FirstOrDefault();
AuthenticationResult msalar = await pca.AcquireTokenAsync(
scopes, user, UIBehavior.ForceLogin, "domain_hint=test.net");
payload = new JObject
{
["access_token"] = msalar.AccessToken
};
mobileServiceClient.LoginAsync(MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload);
Reference: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/660#issuecomment-433831737
Is there a standard way to ask an MVC client using ID4 OIDC middleware to refresh the ClaimsPrincipal when using cookies? It would be nice to just ask the middleware to refresh the ClaimsPrincipal but I don't think that functionality exists.
The code below does work, however, there is no nonce used in the example below - so I'm not sure if that's secure. I'm not sure how the middleware creates the nonce.
Does anyone have an example of properly refreshing the ClaimsPrincipal in an MVC client application using cookies with ID4 OIDC middleware?
Validate ID token and Return ClaimsPrincipal from ID Token
private ClaimsPrincipal ValidateIdentityToken(string idToken, DiscoveryResponse disco )
{
var keys = new List<SecurityKey>();
foreach (var webKey in disco.KeySet.Keys)
{
var e = Base64Url.Decode(webKey.E);
var n = Base64Url.Decode(webKey.N);
var key = new RsaSecurityKey(new RSAParameters { Exponent = e, Modulus = n });
key.KeyId = webKey.Kid;
keys.Add(key);
}
var parameters = new TokenValidationParameters
{
ValidIssuer = disco.TryGetString(OidcConstants.Discovery.Issuer),
ValidAudience = "mvc.hybrid",
IssuerSigningKeys = keys,
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role
};
var handler = new JwtSecurityTokenHandler();
handler.InboundClaimTypeMap.Clear();
SecurityToken token;
var user = handler.ValidateToken(idToken, parameters, out token);
//var nonce = user.FindFirst("nonce")?.Value ?? "";
//if (!string.Equals(nonce, "random_nonce")) throw new Exception("invalid nonce");
//nonce is always ""
return user;
}
Check cookie expiration. Use the refresh token to refresh ID and Access token. Use the above validation to return ClaimsPrincipal
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies",
AutomaticAuthenticate = true,
ExpireTimeSpan = TimeSpan.FromMinutes(60),
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = async cookiecontext =>
{
if (cookiecontext.Properties.Items.ContainsKey(".Token.expires_at"))
{
var expire = DateTime.Parse(cookiecontext.Properties.Items[".Token.expires_at"]);
if (expire <= DateTime.Now.AddMinutes(-5) || DateTime.Now > expire)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
if (disco.IsError) throw new Exception(disco.Error);
var refreshToken = cookiecontext.Properties.Items[".Token.refresh_token"];
var tokenClient = new TokenClient(disco.TokenEndpoint,
"mvc.hybrid",
"secret");
var response = await tokenClient.RequestRefreshTokenAsync(refreshToken);
if (!response.IsError)
{
cookiecontext.Properties.Items[".Token.access_token"] = response.AccessToken;
cookiecontext.Properties.Items[".Token.refresh_token"] = response.RefreshToken;
cookiecontext.Properties.Items[".Token.expires_at"] = DateTime.Now.AddSeconds((int)response.ExpiresIn).ToString();
cookiecontext.Properties.Items["NextAccessTokenRefresh"] = DateTime.Now.AddMinutes(5).ToString();
var _Princ = ValidateIdentityToken(response.IdentityToken, disco);
cookiecontext.ReplacePrincipal(_Princ);
cookiecontext.ShouldRenew = true;
}
else
{
cookiecontext.RejectPrincipal();
}
}
}
}
}
});
Wireup ID4 middleware
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ClientId = "mvc.hybrid",
ClientSecret = "secret",
ResponseType = "code id_token",
Scope = { "openid", "profile", "email", "api1", "offline_access", "role" },
GetClaimsFromUserInfoEndpoint = true,
Events = new OpenIdConnectEvents()
{
OnTicketReceived = notification =>
{
notification.Response.Cookies.Append("NextAccessTokenRefresh", DateTime.Now.AddMinutes(5).ToString());
return Task.FromResult(0);
}
},
SaveTokens = true,
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role,
}
});
}