I'm trying to get dual authentication schemes working with Azure Ad and regular jwt tokens, the issue i'm having is, I can access both controller functions with just the JWT token, regardless of scheme. I tried setting up my configuration from this post but it didn't work (neither does my current configuration obviously):
Authenticating tokens from multiple sources (e.g Cognito and Azure)
[HttpGet]
[Authorize(AuthenticationSchemes = "TestBearer")]
[Route("Test1")]
public async Task Test()
{
}
[HttpGet]
[Authorize(AuthenticationSchemes = "AzureAd")]
[Route("Test2")]
public async Task Test2()
{
}
Startup config
services.AddAuthentication(options =>
{
options.DefaultScheme = "AzureAd";
})
.AddJwtBearer("TestBearer", x =>
{
x.RequireHttpsMetadata = false;
x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Guid.Parse(jwtConfig.SecretKey).ToByteArray())
};
})
.AddMicrosoftIdentityWebApi(x =>
{
x.Audience = config.Audience;
x.Authority = config.Authority;
x.RequireHttpsMetadata = false;
},
x =>
{
x.Instance = config.Instance;
x.ClientId = config.ClientId;
x.TenantId = config.TenantId;
x.Authority = config.Authority;
x.RequireHttpsMetadata = false;
}, "AzureAd");
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
"TestBearer", "AzureAd");
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
var scopes = new string[] { "https://graph.microsoft.com/.default" };
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(config.ClientId)
.WithTenantId(config.TenantId)
.WithClientSecret(config.ClientSecret)
.Build();
//Build the Microsoft Graph client.As the authentication provider, set an async lambda
// which uses the MSAL client to obtain an app-only access token to Microsoft Graph,
// and inserts this access token in the Authorization header of each API request.
var authenticationProvider = (new DelegateAuthenticationProvider(async (requestMessage) =>
{
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await confidentialClientApplication
.AcquireTokenForClient(scopes)
.ExecuteAsync();
//// Add the access token in the Authorization header of the API request.
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
}));
services.AddScoped(_ => new GraphServiceClient(authenticationProvider));
my jwt token generation
private string GenerateToken(TestEntity entity)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Guid.Parse(_jwtSettings.SecretKey).ToByteArray();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]{
new Claim(ClaimTypes.NameIdentifier, entity.Id.ToString()),
new Claim(ClaimTypes.MobilePhone, entity.Phone.ToString()),
new Claim(ClaimTypes.Email, entity.Email)
}),
Expires = DateTime.UtcNow.AddHours(_jwtSettings.ExpiryTimeInHours),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
Audience = "https://localhost:5001",
Issuer = _jwtSettings.Issuer,
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
So I ended up seperating the policies (leaving azure AD as my default policy) and creating an extra combined one in cases I want one of multiple policies to work. All seems to work correctly now.
services.AddAuthorization(options =>
{
options.AddPolicy("Test", policy =>
{
policy.AuthenticationSchemes.Add("TestBearer");
policy.RequireAuthenticatedUser();
});
options.AddPolicy("AzureOrTest", policy =>
{
policy.AuthenticationSchemes.Add("TestBearer");
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
Related
i faced issues when i tried to extend role based authorization by using Microsoft identity.
when call login action method it will generate token if not added role into claim List.
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name,user.UserName),
new Claim(ClaimTypes.NameIdentifier,user.Id.ToString())
};
var roles = await _userManager.GetRolesAsync(user); // If remove this await call then it work fine.
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
After add new role into Claims while generate Token and i did get any response from server for few min after some time it will shown error like below.
Error is : Exception has been thrown by the target of an invocation.
In Startup.cs class under ConfigureServices Method.
services.AddDbContext<DataContext>(x => x.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
{
opt.Password.RequireDigit = false;
opt.Password.RequiredLength = 4;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequireUppercase = false;
});
builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
builder.AddEntityFrameworkStores<DataContext>();
builder.AddRoleValidator<RoleValidator<Role>>();
builder.AddRoleManager<RoleManager<Role>>();
builder.AddSignInManager<SignInManager<User>>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(auth =>
{
auth.TokenValidationParameters = new TokenValidationParameters()
{
// ValidateIssuer = true,
// ValidIssuer = Configuration["AuthSettings:Issuer"],
// ValidateAudience = true,
// ValidAudience = Configuration["AuthSettings:Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["AuthSettings:Key"])),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddControllers(opt =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
opt.Filters.Add(new AuthorizeFilter(policy));
})
.AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
In AuthController
[HttpPost("login")]
public async Task<IActionResult> Login(UserForLoginDto loginDto)
{
var user = await _userManager.FindByNameAsync(loginDto.Username);
var result = await _signInManager.CheckPasswordSignInAsync(user, loginDto.Password, false);
if (result.Succeeded)
{
var appUsers = await _userManager.Users.Include(e => e.Photos)
.FirstOrDefaultAsync(next => next.NormalizedUserName == loginDto.Username.ToUpper());
var userToReturn = _mapper.Map<UserForListDto>(appUsers);
return Ok(new
{
token = GeneratejwtToken(appUsers),
user = userToReturn
});
}
else
{
return Unauthorized();
}
}
private async Task<string> GeneratejwtToken(User user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name,user.UserName),
new Claim(ClaimTypes.NameIdentifier,user.Id.ToString())
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["AuthSettings:Key"]));
var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = signingCredentials
};
var TokenHandler = new JwtSecurityTokenHandler();
var token = TokenHandler.CreateToken(tokenDescriptor);
return TokenHandler.WriteToken(token);
}
I fixed that issue,
while calling GeneratejwtToken method i forgot use await keyword because that method is async method.
token = await GeneratejwtToken(appUsers)
I couldn't understand why I always get 401 unauthorized where the user I logged in has a role of SuperAdmin. I tried looking at other solutions and projects and they seem identical to the code I have still does not work. I use Postman to test the API and in Authorization tab bearer token I pasted the token of the user I logged in and make a request on this API.
//API
[Route("create")]
[Authorize(Roles = "SuperAdmin")]
public async Task<IActionResult> RegisterUserAsync([FromBody] Request request)
{
return something;
}
//StartUp.cs
private void ConfigureAuth(IServiceCollection services)
{
services.AddIdentity<UserEntity, RoleEntity>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddRoles<RoleEntity>();
}
var key = Encoding.ASCII.GetBytes(jwtSettings.Secret);
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 = false,
ValidateAudience = false
};
});
//JWT
public string GenerateToken(UserEntity userEntity, IList<string> roles)
{
var token = string.Empty;
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.jwtOptions.GetJwtOptions().Secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, userEntity.UserName),
new Claim(ClaimTypes.GivenName, userEntity.FirstName),
new Claim(ClaimTypes.Surname, userEntity.LastName),
new Claim(ClaimTypes.NameIdentifier, userEntity.Id.ToString()),
new Claim(ClaimTypes.Role, roles.FirstOrDefault()) //SuperAdmin
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(this.jwtOptions.GetJwtOptions().ExpiresInMinutes),
SigningCredentials = credentials
};
token = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
return token;
}
You need to add app.UseAuthentication() before app.UseAuthorization() , the authentication middleware will handle the JWT bearer authentication , validate and decode token , at last write to user's principle.
Using .Net Core 3.1 WebApi with Jwt authentication seems to work fine unless we try to use ValidateIssuer and Validate Audience.
When we set these properties to true, we get an Unauthorized Http Status code.
We get the values for Audience and Issuer from our app settings, so we know they are the same.
Following is the code from our startup.cs:
//
// Configure JWT authentication from the 'jwtIssuerOptions' values in the appsettings.json file
//
Models.JwtIssuerOptions jwtSettings = _appConfiguration.GetSection("jwtIssuerOptions").Get<Models.JwtIssuerOptions>();
var keyBytes = Encoding.UTF8.GetBytes(jwtSettings.JwtSecret);
SymmetricSecurityKey symmetricSecurityKey = new SymmetricSecurityKey(keyBytes);
services.AddAuthentication(a =>
{
a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(b =>
{
b.RequireHttpsMetadata = false;
b.SaveToken = true;
b.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = jwtSettings.Audience ,
ValidIssuer = jwtSettings.Issuer ,
ValidateIssuerSigningKey = true,
IssuerSigningKey = symmetricSecurityKey,
TokenDecryptionKey = symmetricSecurityKey,
ValidateIssuer = true,
ValidateAudience = true
};
});
Following is the code from our Auth Helper that creates the Jwt:
private void CreateTheJWT(EndUserCredentials user)
{
var keyBytes = Encoding.UTF8.GetBytes(_jwtIssuerOptions.JwtSecret);
var symmetricSecurityKey = new SymmetricSecurityKey(keyBytes);
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256Signature);
var cryptoKey = new EncryptingCredentials(symmetricSecurityKey, JwtConstants.DirectKeyUseAlg, SecurityAlgorithms.Aes256CbcHmacSha512);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Name),
new Claim("EndUser", System.Text.Json.JsonSerializer.Serialize( user)),
}),
Expires = DateTime.UtcNow.AddMinutes(_jwtIssuerOptions.TimeoutMinutes),
Audience = _jwtIssuerOptions.Issuer,
Issuer = _jwtIssuerOptions.Audience,
NotBefore = DateTime.UtcNow.AddMinutes(-2),
IssuedAt = DateTime.UtcNow.AddMinutes(-1),
SigningCredentials = signingCredentials,
EncryptingCredentials = cryptoKey
};
foreach (string role in user.Roles)
{
tokenDescriptor.Subject.AddClaim(new Claim(ClaimTypes.Role, role));
}
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
}
When we set the offending properties to "false", everything works. Any help is appreciated!
it seems to me that your issuer and audiance are swapped. can you reassign in CreateTheJWT function?
Audience = _jwtIssuerOptions.Audience,
Issuer = _jwtIssuerOptions.Issuer,
If you are here like me and you didn't swap your issuer and audience.
Ensure you have this in your Startup.ConfigureServices.
app.UseAuthentication();
app.UseAuthorization();
I have created role-based authentication. Token is generated succesfully.
In my vue app, after login i m adding token to localStorage and then, im sending it in header by use of axios in format:
Authorization: "Bearer"
I m getting unauthorize everytime i m trying to do GET on items.
In postman is this same.
I m using IIS to host my app localy, and as frontend i m using Vue.js
I have already tried:
- authorization and Authorization in header
- changing http to https and vice versa.
Startup ConfigureServices:
#region JWT
var key = Encoding.ASCII.GetBytes(Configuration.GetValue<string>("SecretKey"));
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 = false,
ValidateAudience = false
};
});
#endregion
Startup Configure:
app.UseAuthentication();
My method to generate token based on roles:
public User Authenticate(string login, string password)
{
var user = Context.Users.SingleOrDefault(p => p.Login == login && p.Password == password);
if (user == null)
return null;
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString()),
new Claim(ClaimTypes.Role, user.Role)
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
user.Password = null;
return user;
}
And this is my Endpoint (in case if something is wrong here)
[Route("api/[controller]")]
[EnableCors("MyPolicy")]
[Authorize]
[ApiController]
public class ItemsController : ControllerBase
{
public ItemsController()
{
}
[HttpGet]
[Authorize(Roles = "Admin")]
public ActionResult<IEnumerable<Item>> Get()
{
var items = new ItemsService();
return items
.GetItems();
}
If you refer to https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x?view=aspnetcore-2.2 you can see you can configure OpenID Connect (OIDC) authentication for various providers as below:
Facebook
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = Configuration["auth:facebook:appid"];
options.AppSecret = Configuration["auth:facebook:appsecret"];
});
Google
services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = Configuration["auth:google:clientid"];
options.ClientSecret = Configuration["auth:google:clientsecret"];
});
Microsoft
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["auth:microsoft:clientid"];
options.ClientSecret = Configuration["auth:microsoft:clientsecret"];
});
My question is does anybody have the settings I would need to provide to Support Amazon and Evernote OIDC?
You can find Login with Amazon reference here
Amazon still does not support OIDC, but supports OAuth. However the default OAuthHandler for dotnet does not provide UserInfoEndpoint handling. That's why you have to either implement the call to UserInfoEndpoint (can grab it from oidc) or hack the OIDC to make it thinking that it has id_token whenever it has not. I've passed the second route. Little bit dirty trick, but I've got my user identified.
.AddOpenIdConnect("lwa", "LoginWithAmazon", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://www.amazon.com/";
options.ClientId = "amzn1.application-oa2-client.xxxxxxxxxxxxxx";
options.ClientSecret = "xxxxxxxxxxxxxxxxx";
options.ResponseType = "code";
options.ResponseMode = "query";
options.SaveTokens = true;
options.CallbackPath = "/signin-amazon";
options.SignedOutCallbackPath = "/signout-callback-amazon";
options.RemoteSignOutPath = "/signout-amazon";
options.Scope.Clear();
options.Scope.Add("profile");
options.GetClaimsFromUserInfoEndpoint = true;
var rsa = RSA.Create();
var key = new RsaSecurityKey(rsa){KeyId = "1"};
var jwtClaims = new List<Claim>
{
new Claim(JwtClaimTypes.IssuedAt, "now"),
new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()),
new Claim(JwtClaimTypes.Subject, Guid.NewGuid().ToString())
};
var jwt = new JwtSecurityToken(
"issuer",
"audience",
jwtClaims,
DateTime.UtcNow,
DateTime.UtcNow.AddHours(1),
new SigningCredentials(key, "RS256"));
var handler = new JwtSecurityTokenHandler();
handler.OutboundClaimTypeMap.Clear();
var token = handler.WriteToken(jwt);
options.Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://www.amazon.com/ap/oa",
TokenEndpoint = "https://api.amazon.com/auth/o2/token",
UserInfoEndpoint = "https://api.amazon.com/user/profile"
};
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateTokenReplay = false,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
IssuerSigningKey = key
};
AuthorizationCodeReceivedContext hook = null;
options.Events = new OpenIdConnectEvents
{
OnAuthenticationFailed = async context =>
{
//context.SkipHandler();
},
OnAuthorizationCodeReceived = async context => { hook = context; },
OnTokenResponseReceived = async context =>
{
context.TokenEndpointResponse.IdToken = token;
hook.TokenEndpointResponse = context.TokenEndpointResponse;
},
OnUserInformationReceived = async context =>
{
var user = context.User;
var claims = new[]
{
new Claim(JwtClaimTypes.Subject, user["user_id"].ToString()),
new Claim(JwtClaimTypes.Email, user["email"].ToString()),
new Claim(JwtClaimTypes.Name, user["name"].ToString())
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
context.Success();
}
};
})
Unfortunately, neither Login with Amazon nor Evernote supports Open ID Connect. Other mentioned services do, which can be verified by visiting appropriate configuration site of each of them: Google, Microsoft.
There are ofc others that are not preconfigured in .Net and can be used with it:
Salesforce
As you probably noticed usually the configuration for Open ID Connect is stored on a site with "/.well-known/openid-configuration" suffix. This is called OpenID Connect metadata document and it contains most of the information required for an app to do sign-in. This includes information such as the URLs to use and the location of the service's public signing keys.
And now lets go for .Net configuration for custom Open ID Connect provider (I will use Salesforce as it supports Open ID):
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = Configuration["auth:facebook:appid"];
options.AppSecret = Configuration["auth:facebook:appsecret"];
})
.AddOpenIdConnect("OpenIdConnectSalesforce", "Salesforce", options =>
{
options.Authority = "https://login.salesforce.com";
options.ClientId = Configuration["auth:salesforce:appid"];
options.ClientSecret = Configuration["auth:salesforce:appsecret"];
options.ResponseType = "code";
});
And after launching web app we can see additional button to log in using Salesforce:
As for Evernote and Amazon you could use their SDKs and APIs to implement their log in methods respectively. I do believe that they support OAuth.
Extended the solution by #d-f to use OAuth handler.
.AddOAuth("lwa-oauth", "OauthLoginWithAmazon", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "amzn1.application-oa2-client.zzzzzzzzzzzz";
options.ClientSecret = "4c0630b4166c901519a730835ezzzzzzzzzzzzzzzz";
options.SaveTokens = true;
options.CallbackPath = "/signin-amazon";
options.Scope.Clear();
options.Scope.Add("profile");
options.AuthorizationEndpoint = "https://www.amazon.com/ap/oa";
options.TokenEndpoint = "https://api.amazon.com/auth/o2/token";
options.UserInformationEndpoint = "https://api.amazon.com/user/profile";
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var accessToken = context.AccessToken;
HttpResponseMessage responseMessage =
await context.Backchannel.SendAsync(
new HttpRequestMessage(HttpMethod.Get, options.UserInformationEndpoint)
{
Headers =
{
Authorization = new AuthenticationHeaderValue("Bearer", accessToken)
}
});
responseMessage.EnsureSuccessStatusCode();
string userInfoResponse = await responseMessage.Content.ReadAsStringAsync();
var user = JObject.Parse(userInfoResponse);
var claims = new[]
{
new Claim(JwtClaimTypes.Subject, user["user_id"].ToString()),
new Claim(JwtClaimTypes.Email, user["email"].ToString()),
new Claim(JwtClaimTypes.Name, user["name"].ToString())
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
context.Success();
}
};
})