I need to consume ASP.NET 5 secure Web API from a web client using local accounts. In the past there was a handler issuing bearer tokens in order to support OAuth, now bearer token issuance responsibility was removed from identity. Some people recommends to use identityServer3, which require clients registration, unlike identity2 approach. What is the simplest way to achieve authorization in ASP.NET 5 Web API? How can I avoid to pass client id and client secret in order to get an access token when using resource owner password flow? How to call api avoiding to pass scope?
I have built a simple bearer token issuer from this but using Identity password hasher. You can see full code below:
public class TokenController : Controller
{
private readonly IBearerTokenGenerator generator;
private readonly IClientsManager clientsManager;
private readonly IOptions<TokenAuthOptions> options;
public TokenController(IBearerTokenGenerator generator,
IClientsManager clientsManager,
IOptions<TokenAuthOptions> options)
{
this.generator = generator;
this.clientsManager = clientsManager;
this.options = options;
}
[HttpPost, AllowAnonymous]
public IActionResult Post(AuthenticationViewModel req)
{
return clientsManager
.Find(req.client_id, req.client_secret)
.Map(c => c.Client)
.Map(c => (IActionResult)new ObjectResult(new {
access_token = generator.Generate(c),
expires_in = options.Value.ExpirationDelay.TotalSeconds,
token_type = "Bearer"
}))
.ValueOrDefault(HttpUnauthorized);
}
}
public class BearerTokenGenerator : IBearerTokenGenerator
{
private readonly IOptions<TokenAuthOptions> tokenOptions;
public BearerTokenGenerator(IOptions<TokenAuthOptions> tokenOptions)
{
this.tokenOptions = tokenOptions;
}
public string Generate(Client client)
{
var expires = Clock.UtcNow.Add(tokenOptions.Value.ExpirationDelay);
var handler = new JwtSecurityTokenHandler();
var identity = new ClaimsIdentity(new GenericIdentity(client.Identifier, "TokenAuth"), new Claim[] {
new Claim("client_id", client.Identifier)
});
var securityToken = handler.CreateToken(
issuer: tokenOptions.Value.Issuer,
audience: tokenOptions.Value.Audience,
signingCredentials: tokenOptions.Value.SigningCredentials,
subject: identity,
expires: expires);
return handler.WriteToken(securityToken);
}
}
public class ClientsManager : IClientsManager
{
private readonly MembershipDataContext db;
private readonly ISecretHasher hasher;
public ClientsManager(MembershipDataContext db,
ISecretHasher hasher)
{
this.db = db;
this.hasher = hasher;
}
public void Create(string name, string identifier, string secret, Company company)
{
var client = new Client(name, identifier, company);
db.Clients.Add(client);
var hash = hasher.HashSecret(secret);
var apiClient = new ApiClient(client, hash);
db.ApiClients.Add(apiClient);
}
public Option<ApiClient> Find(string identifier, string secret)
{
return FindByIdentifier(identifier)
.Where(c => hasher.Verify(c.SecretHash, secret));
}
public void ChangeSecret(string identifier, string secret)
{
var client = FindByIdentifier(identifier).ValueOrDefault(() => {
throw new ArgumentException($"could not find any client with identifier { identifier }");
});
var hash = hasher.HashSecret(secret);
client.ChangeSecret(hash);
}
public string GenerateRandomSecret()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var generated = new string(Enumerable.Repeat(chars, 12).Select(s => s[random.Next(s.Length)]).ToArray());
return Convert.ToBase64String(Encoding.UTF8.GetBytes(generated));
}
private Option<ApiClient> FindByIdentifier(string identifier)
{
return db.ApiClients
.Include(c => c.Client)
.SingleOrDefault(c => c.Client.Identifier == identifier)
.ToOptionByRef();
}
}
public class SecretHasher : ISecretHasher
{
private static Company fakeCompany = new Company("fake", "fake");
private static Client fakeClient = new Client("fake", "fake", fakeCompany);
private readonly IPasswordHasher<Client> hasher;
public SecretHasher(IPasswordHasher<Client> hasher)
{
this.hasher = hasher;
}
public string HashSecret(string secret)
{
return hasher.HashPassword(fakeClient, secret);
}
public bool Verify(string hashed, string secret)
{
return hasher.VerifyHashedPassword(fakeClient, hashed, secret) == PasswordVerificationResult.Success;
}
}
Now in Startup.cs
services.Configure<TokenAuthOptions>(options =>
{
options.Audience = "API";
options.Issuer = "Web-App";
options.SigningCredentials = new SigningCredentials(GetSigninKey(), SecurityAlgorithms.RsaSha256Signature);
options.ExpirationDelay = TimeSpan.FromDays(365);
});
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
}
app.UseJwtBearerAuthentication(options =>
{
options.TokenValidationParameters.IssuerSigningKey = GetSigninKey();
options.TokenValidationParameters.ValidAudience = "API";
options.TokenValidationParameters.ValidIssuer = "Web-App";
options.TokenValidationParameters.ValidateSignature = true;
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(0);
});
Related
In Program.cs I have a post endpoint for getting a Jwt token:
app.MapPost("/api/security/getToken", [AllowAnonymous] async (UserManager<IdentityUser> userMgr, UserLoginDTO user) => {
var identityUser = await userMgr.FindByEmailAsync(user.Email);
if (await userMgr.CheckPasswordAsync(identityUser, user.Password)) {
var issuer = builder.Configuration["Jwt:Issuer"];
var audience = builder.Configuration["Jwt:Audience"];
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim> {
new Claim(ClaimTypes.Email, identityUser.Email),
new Claim(ClaimTypes.GivenName, identityUser.UserName)
};;
var token = new JwtSecurityToken(issuer: issuer, audience: audience, signingCredentials: credentials, claims: claims);
var tokenHandler = new JwtSecurityTokenHandler();
var stringToken = tokenHandler.WriteToken(token);
return Results.Ok(stringToken);
}
else {
return Results.Unauthorized();
}
}).WithTags("Security");
I want to move that logic in a separate class for readability.
public static class SecurityEndpoints {
public static void MapSecurityEndpoints(this WebApplication app) {
app.MapPost("/api/security/getToken", GetToken).AllowAnonymous();
app.MapPost("/api/security/createUser", CreateUser).AllowAnonymous();
}
internal static async Task<IResult> GetToken(this WebApplicationBuilder builder, UserManager<IdentityUser> userMgr,[FromBody] UserLoginDTO user) {
//same logic
}
...
}
Because of the WebApplicationBuilder I'm getting a InvalidOperationException.
How to refactor the GetToken method to get rid of WebApplicationBuilder and maybe access the builder.Configuration details that I need form another service? How would that service look like?
Or how else would you approach this?
You should not use WebApplication(Builder) in your handlers. Use standard approaches for working with the configuration in .NET/ASP.NET Core which are described here.
For example create type for jwt settings, register it with options pattern and inject into handler. Something along this lines:
public class JwtOptions
{
public const string Position = "Jwt";
public string Issuer { get; set; } = String.Empty;
public string Audience { get; set; } = String.Empty;
}
builder.Services.Configure<JwtOptions>(
builder.Configuration.GetSection(JwtOptions.Position));
internal static async Task<IResult> GetToken(IOptions<JwtOptions> jwtOptions,...) {
// use jwtOptions
}
I've posted this question before but being new I wanted to learn how to write a "proper" question (in case you've tried to help me and I didn't get back immediately, wanted to get it mostly right if not all hopefully I've got the hang of it)
This is just registration/login code whose purpose is self explanatory, the error am getting (could not load file or assembly 'microsoft aspnetcore razor runtime 3.1 1) happens in a service registration class in the AddControllers() method, I've tried adding the package specified in the error but it didn't work, I tried a few other similar packages but the result has been the same, when I build I get no errors or warnings but when the app runs I get the error, hope this is enough data to work with.
//controller class Login and registration
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class IdentifyMe : Controller
{
private readonly IIdentify _identify;
public IdentifyMe(IIdentify identifying)
{
_identify = identifying;
}
[HttpGet(Api_Routes.Identity.Register)]
public async Task<IActionResult> Register(UserRegistration register)
{
if (!ModelState.IsValid)
{
return BadRequest(new Unauthenticated
{
Errors=ModelState.Values.SelectMany(x=>x.Errors.Select(xx=>xx.ErrorMessage))
});
}
var authresponce = await _identify.RegisterAsync(register.Email, register.Password, register.User_Name);
if (!authresponce.Success)
{
return BadRequest(new Unauthenticated
{
Errors = authresponce.Errors
});
}
return Ok(new Authenticated
{
Token = authresponce.Token
});
}
[HttpGet(Api_Routes.Identity.Login)]
public async Task<IActionResult> LoginAsync(User_login login)
{
var authresponce = await _identify.LoginAsync(login.Password, login.email);
if (!authresponce.Success)
{
return BadRequest(new Unauthenticated
{
Errors = authresponce.Errors
});
}
return Ok(new Authenticated
{
Token = authresponce.Token
});
}
}
// service registration
public class Dbinstaller : IInstaller
{
public void Cleanner(IConfiguration configuration, IServiceCollection services)
{
var jwt = new JWTsettings();
configuration.Bind(nameof(jwt), jwt);
services.AddSingleton(jwt);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.SaveToken = true;
var TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwt.Secret)),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true
};
});
services.AddSwaggerGen(x =>
{
x.SwaggerDoc("v1", new OpenApiInfo { Title = "TXC API", Version = "v1" });
var security = new Dictionary<string, IEnumerable<string>>
{
{"Bearer", new string[0]}
};
x.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description="JWT Authorization Header using the bearer scheme",
Name="Authorization",
In=ParameterLocation.Header,
Type=SecuritySchemeType.ApiKey
});
x.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{new OpenApiSecurityScheme{Reference=new OpenApiReference
{
Id="Bearer",
Type=ReferenceType.SecurityScheme
}
},new List<string>() }
});
});
services.AddDbContext<DataContext>(opt =>
opt.UseSqlServer(configuration.GetConnectionString("TXC Connection")));
services.AddIdentityCore<IdentityUser>();
}
}
//service registration, specified error occurs in Add controllers()
public class In_App_Componentes : IInstaller
{
public void Cleanner(IConfiguration configuration, IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson(p => p.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
services.AddScoped<IX_Change, The_center>();
services.AddScoped<IIdentify, Identify>();
}
}
//service implementation class
public class Identify : IIdentify
{
private readonly UserManager<IdentityUser> _manager;
private readonly JWTsettings jwtset;
public Identify(UserManager<IdentityUser> userManager, JWTsettings jW)
{
_manager = userManager;
jwtset = jW;
}
public async Task<Authentication_result> RegisterAsync(string email, string password, string Username)
{
var exists = _manager.FindByEmailAsync(email);
if (exists != null)
{
return new Authentication_result
{
Errors = new[] { "User with this email already exists" }
};
}
var newPerson = new IdentityUser
{
Email = email,
UserName = Username
};
var Creation = await _manager.CreateAsync(newPerson, password);
if (!Creation.Succeeded)
{
return new Authentication_result
{
Errors = new[] { "Invalid user!" }
};
}
return Generate_Authentication_Result(newPerson);
}
public async Task<Authentication_result> LoginAsync(string email, string Password)
{
var user = await _manager.FindByEmailAsync(email);
if (user == null)
{
return new Authentication_result
{
Errors = new[] { "User does not exists" }
};
}
var validate_password = await _manager.CheckPasswordAsync(user, Password);
if (!validate_password)
{
return new Authentication_result
{
Errors = new[] { "" }
};
}
return Generate_Authentication_Result(user);
}
private Authentication_result Generate_Authentication_Result(IdentityUser newPerson)
{
var Tokenhandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(jwtset.Secret);
var TokenDescripter = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, newPerson.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, newPerson.Email),
new Claim("id",newPerson.Id)
}),
Expires = DateTime.UtcNow.AddHours(2),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = Tokenhandler.CreateToken(TokenDescripter);
return new Authentication_result
{
Success = true,
Token = Tokenhandler.WriteToken(token)
};
}
}
Ciao, I tried to search Microsoft.AspNetCore.Razor.Runtime v. 3.1.1 on NuGet but I found only 2.2.0 . I didn't found any reference to 3.1.1 . Anyway, downgrading Microsoft.AspNetCore.Identity.UI to 3.1.0 should solve your problem as mentioned here
I am developing web site with ASP.NET CORE that uses Claims for User Authentication and User Id and other infos keeps in Claims, is it security ?
ClaimsIdentity identity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.Name, userInfo.Name),
new Claim(ClaimTypes.Surname, userInfo.Surname),
new Claim("Image", userInfo.Image),
new Claim(ClaimTypes.NameIdentifier,result.Id.ToString()),
new Claim(ClaimTypes.IsPersistent, loginViewModel.RememberMe.ToString())
},
CookieName.User);
HttpContext.SignOutAsync(CookieName.User).Wait();
HttpContext.SignInAsync(CookieName.User, new ClaimsPrincipal(identity),
new AuthenticationProperties
{
IsPersistent = loginViewModel.RememberMe,
AllowRefresh = true
}).Wait();
Sometime i need change user infos and it uses this. is it secure way ?
//Get
int id = int.Parse(new ClaimsCookie(HttpContext).GetValue(CookieName.User, KeyName.Id));
//Set Update
new ClaimsCookie(HttpContext).SetValue(CookieName.User, new[] { KeyName.Name, KeyName.Surname }, new[] { model.Name, model.Surname });
Class :
namespace ...
{
public class ClaimsCookie
{
private readonly HttpContext _httpContext;
public ClaimsCookie(HttpContext httpContext)
{
_httpContext = httpContext;
}
public string GetValue(string cookieName, string keyName)
{
var principal = _httpContext.User;
var cp = principal.Identities.First(i => i.AuthenticationType == cookieName.ToString());
return cp.FindFirst(keyName).Value;
}
public async void SetValue(string cookieName, string[] keyName, string[] value)
{
if (keyName.Length != value.Length)
{
return;
}
if (_httpContext == null)
return;
var principal = _httpContext.User;
var cp = principal.Identities.First(i => i.AuthenticationType == cookieName.ToString());
for (int i = 0; i < keyName.Length; i++)
{
if (cp.FindFirst(keyName[i]) != null)
{
cp.RemoveClaim(cp.FindFirst(keyName[i]));
cp.AddClaim(new Claim(keyName[i], value[i]));
}
}
await _httpContext.SignOutAsync(cookieName);
await _httpContext.SignInAsync(cookieName, new ClaimsPrincipal(cp),
new AuthenticationProperties
{
IsPersistent = bool.Parse(cp.FindFirst(KeyName.IsPersistent).Value),
AllowRefresh = true
});
}
public async void SetValue(string cookieName, string keyName, string value)
{
var principal = _httpContext.User;
var cp = principal.Identities.First(i => i.AuthenticationType == cookieName.ToString());
if (cp.FindFirst(keyName) != null)
{
cp.RemoveClaim(cp.FindFirst(keyName));
cp.AddClaim(new Claim(keyName, value));
}
await _httpContext.SignOutAsync(cookieName);
await _httpContext.SignInAsync(cookieName, new ClaimsPrincipal(cp),
new AuthenticationProperties
{
IsPersistent = bool.Parse(cp.FindFirst(KeyName.IsPersistent).Value),
AllowRefresh = true
});
}
}
public static class CookieName
{
public static string Company => "CompanyUserProfilCookie";
public static string User => "UserProfilCookie";
public static string Admin => "AdminPanelCookie";
}
public static class KeyName
{
public static string Id => ClaimTypes.NameIdentifier;
public static string Name => ClaimTypes.Name;
public static string Surname => ClaimTypes.Surname;
public static string IsPersistent => ClaimTypes.IsPersistent;
public static string Image => "Image";
}
}
I am setting HttpContext to this class from any Controller. Have any way static HttpContext, i dont want set from Controller ?
One option is to inject IHttpContextAccessor from DI and access HttpContext from it.
Change ClaimsCookie constructor to reflect that:
private readonly HttpContext _httpContext;
public ClaimCookie(IHttpContextAccessor contextAccessor)
{
_httpContext = contextAccessor.HttpContext;
}
Next you need to register both IHttpContextAccessor and ClaimCookie in Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddTransient<ClaimCookie>();
...rest of code ommited...
}
Then inject Your class and use is without providing HttpContext by yourself:
public class SomeController : Controller
{
private readonly ClaimCookie _claimCookie;
public SomeController(ClaimCookie claimCookie)
{
_claimCookie = claimCookie;
}
public async Task<IActionResult> SomeAction()
{
int id = int.Parse(_claimCookie.GetValue(CookieName.User, KeyName.Id));
await _claimCookie.SetValue(CookieName.User, new[] { KeyName.Name, KeyName.Surname }, new[] { model.Name, model.Surname });
...
}
Also read msdn's best practices in asynchronous programming why you should't use async void.
And about security (im not an expert) you also shouldn't store sensitive data in cookies, if You need so then store encrypted data.
The problem statement
We are developing a new enterprise level application and want to utilize Azure Active Directory for signing into the application so that we do not have to create another set of user credentials. However, our permissions model for this application is more complex than what can be handled via groups inside of AAD.
The thought
The thought was that we could use Azure Active Directory OAuth 2.0 in addition to the ASP.NET Core Identity framework to force users to authenticate through Azure Active Directory and then use the identity framework to handle authorization/permissions.
The Issues
You can create projects out of the box using Azure OpenId authentication and then you can easily add Microsoft account authentication (Not AAD) to any project using Identity framework. But there was nothing built in to add OAuth for AAD to the identity model.
After trying to hack those methods to get them to work like I needed I finally went through trying to home-brew my own solution building off of the OAuthHandler and OAuthOptions classes.
I ran into a lot of issues going down this route but managed to work through most of them. Now I am to a point where I am getting a token back from the endpoint but my ClaimsIdentity doesn't appear to be valid. Then when redirecting to the ExternalLoginCallback my SigninManager is unable to get the external login information.
There almost certainly must be something simple that I am missing but I can't seem to determine what it is.
The Code
Startup.cs
services.AddAuthentication()
.AddAzureAd(options =>
{
options.ClientId = Configuration["AzureAd:ClientId"];
options.AuthorizationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/authorize";
options.TokenEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/token";
options.UserInformationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/openid/userinfo";
options.Resource = Configuration["AzureAd:ClientId"];
options.ClientSecret = Configuration["AzureAd:ClientSecret"];
options.CallbackPath = Configuration["AzureAd:CallbackPath"];
});
AzureADExtensions
namespace Microsoft.AspNetCore.Authentication.AzureAD
{
public static class AzureAdExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
=> builder.AddAzureAd(_ => { });
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
return builder.AddOAuth<AzureAdOptions, AzureAdHandler>(AzureAdDefaults.AuthenticationScheme, AzureAdDefaults.DisplayName, configureOptions);
}
public static ChallengeResult ChallengeAzureAD(this ControllerBase controllerBase, SignInManager<ApplicationUser> signInManager, string redirectUrl)
{
return controllerBase.Challenge(signInManager.ConfigureExternalAuthenticationProperties(AzureAdDefaults.AuthenticationScheme, redirectUrl), AzureAdDefaults.AuthenticationScheme);
}
}
}
AzureADOptions & Defaults
public class AzureAdOptions : OAuthOptions
{
public string Instance { get; set; }
public string Resource { get; set; }
public string TenantId { get; set; }
public AzureAdOptions()
{
CallbackPath = new PathString("/signin-azureAd");
AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint;
TokenEndpoint = AzureAdDefaults.TokenEndpoint;
UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint;
Scope.Add("https://graph.windows.net/user.read");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "unique_name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "given_name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "family_name");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/groups", "groups");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/identity/claims/objectidentifier", "oid");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "roles");
}
}
public static class AzureAdDefaults
{
public static readonly string DisplayName = "AzureAD";
public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize";
public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
public static readonly string UserInformationEndpoint = "https://login.microsoftonline.com/common/openid/userinfo"; // "https://graph.windows.net/v1.0/me";
public const string AuthenticationScheme = "AzureAD";
}
AzureADHandler
internal class AzureAdHandler : OAuthHandler<AzureAdOptions>
{
public AzureAdHandler(IOptionsMonitor<AzureAdOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
{
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
HttpResponseMessage httpResponseMessage = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted);
if (!httpResponseMessage.IsSuccessStatusCode)
throw new HttpRequestException(message: $"Failed to retrived Azure AD user information ({httpResponseMessage.StatusCode}) Please check if the authentication information is correct and the corresponding Microsoft Account API is enabled.");
JObject user = JObject.Parse(await httpResponseMessage.Content.ReadAsStringAsync());
OAuthCreatingTicketContext context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, user);
context.RunClaimActions();
await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
}
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("grant_type", "authorization_code");
dictionary.Add("client_id", Options.ClientId);
dictionary.Add("redirect_uri", redirectUri);
dictionary.Add("client_secret", Options.ClientSecret);
dictionary.Add(nameof(code), code);
dictionary.Add("resource", Options.Resource);
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequestMessage.Content = new FormUrlEncodedContent(dictionary);
HttpResponseMessage response = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted);
if (response.IsSuccessStatusCode)
return OAuthTokenResponse.Success(JObject.Parse(await response.Content.ReadAsStringAsync()));
return OAuthTokenResponse.Failed(new Exception(string.Concat("OAuth token endpoint failure: ", await Display(response))));
}
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("client_id", Options.ClientId);
dictionary.Add("scope", FormatScope());
dictionary.Add("response_type", "code");
dictionary.Add("redirect_uri", redirectUri);
dictionary.Add("state", Options.StateDataFormat.Protect(properties));
dictionary.Add("resource", Options.Resource);
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, dictionary);
}
private static async Task<string> Display(HttpResponseMessage response)
{
StringBuilder output = new StringBuilder();
output.Append($"Status: { response.StatusCode };");
output.Append($"Headers: { response.Headers.ToString() };");
output.Append($"Body: { await response.Content.ReadAsStringAsync() };");
return output.ToString();
}
}
AccountController.cs
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> SignIn()
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account");
return this.ChallengeAzureAD(_signInManager, redirectUrl);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
_logger.LogInformation($"Error from external provider: {remoteError}");
return RedirectToAction(nameof(SignedOut));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null) //This always ends up true!
{
return RedirectToAction(nameof(SignedOut));
}
}
There you have it!
This is the code I have, and I'm almost sure that at this point there is something simple I am missing but am unsure of what it is. I know that my CreateTicketAsync method is problematic as well since I'm not hitting the correct user information endpoint (or hitting it correctly) but that's another problem all together as from what I understand the claims I care about should come back as part of the token.
Any assistance would be greatly appreciated!
I ended up resolving my own problem as it ended up being several issues. I was passing the wrong value in for the resource field, hadn't set my NameIdentifer mapping correctly and then had the wrong endpoint for pulling down user information. The user information piece being the biggest as that is the token I found out that the external login piece was looking for.
Updated Code
Startup.cs
services.AddAuthentication()
.AddAzureAd(options =>
{
options.ClientId = Configuration["AzureAd:ClientId"];
options.AuthorizationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/authorize";
options.TokenEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/token";
options.ClientSecret = Configuration["AzureAd:ClientSecret"];
options.CallbackPath = Configuration["AzureAd:CallbackPath"];
});
AzureADOptions & Defaults
public class AzureAdOptions : OAuthOptions
{
public string Instance { get; set; }
public string Resource { get; set; }
public string TenantId { get; set; }
public AzureAdOptions()
{
CallbackPath = new PathString("/signin-azureAd");
AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint;
TokenEndpoint = AzureAdDefaults.TokenEndpoint;
UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint;
Resource = AzureAdDefaults.Resource;
Scope.Add("user.read");
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName");
ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName");
ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname");
ClaimActions.MapJsonKey(ClaimTypes.MobilePhone, "mobilePhone");
ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value<string>("mail") ?? user.Value<string>("userPrincipalName"));
}
}
public static class AzureAdDefaults
{
public static readonly string DisplayName = "AzureAD";
public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize";
public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
public static readonly string Resource = "https://graph.microsoft.com";
public static readonly string UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me";
public const string AuthenticationScheme = "AzureAD";
}
I'm quite new to Web API and i still don't get it how to request n JWT.
In My Startup.cs i configured OAuth and my custom JWT:
CustomJwtFormat.cs
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly string issuer = string.Empty;
private readonly int timeoutMinutes = 60;
public CustomJwtFormat(string issuer)
{
this.issuer = issuer;
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
string audience = "all";
var secret = Convert.FromBase64String("mySecret");
var now = DateTime.UtcNow;
var expires = now.AddMinutes(timeoutMinutes);
string signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256";
string digestAlgorithm = "http://www.w3.org/2001/04/xmlenc#sha256";
var signingKey = new SigningCredentials(
new InMemorySymmetricSecurityKey(secret), signatureAlgorithm, digestAlgorithm);
var token = new JwtSecurityToken(issuer, audience, data.Identity.Claims,
now, expires, signingKey);
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
return tokenString;
}
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}
EDIT: I managed retrieving a token with Postman.
Now my next problem: as long as i send a valid jwt everything works ok. But whenn i send e.g. 'bearer 12345' i get the error message
IDX10708: 'System.IdentityModel.Tokens.JwtSecurityTokenHandler' cannot read this string: '12345'.
...in Visual Studio. Where do i need to put in the token validation? Make a new class and put the TokenSecurityHandler anywhere?