I understand it's a long question but I would really appreciate it if anyone could share their thoughts or experience with me as I've been around this for a few days now trying lots of things. I'm having an asp net core 3.1 web API application and an ASP.NET Core 3.1 MVC application.
Both have been registered in Azure AD. The API project is supposed to create calendar events based on the request payload it receives from the MVC project. I am following the Microsoft instructions from this link here
But once the API project makes a call against the Microsoft Graph, it fails with the following error:
"code": "InvalidAuthenticationToken",
"message": "Access token validation failure. Invalid audience.",
I'm putting in the minimum here to provide some more info but the whole sample can be downloaded from the link above.
ASP.NET Core MVC Startup.cs:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
AzureAdOptions.Settings = options;
})
.AddCookie();
ASP.NET Core MVC project AddAzureAd function:
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
builder.AddOpenIdConnect();
return builder;
}
ConfigureAzureOptions:
public void Configure(string name, OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = _azureOptions.Authority;
options.UseTokenLifetime = true;
options.CallbackPath = _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false;
options.ClientSecret = _azureOptions.ClientSecret;
options.Resource = "https://graph.microsoft.com"; // AAD graph
// Without overriding the response type (which by default is id_token), the OnAuthorizationCodeReceived event is not called.
// but instead OnTokenValidated event is called. Here we request both so that OnTokenValidated is called first which
// ensures that context.Principal has a non-null value when OnAuthorizeationCodeReceived is called
options.ResponseType = "id_token code";
// Subscribing to the OIDC events
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
And here's the code from the API project to configure Azure Options:
private class ConfigureAzureOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
{
_azureOptions = azureOptions.Value;
}
public void Configure(string name, JwtBearerOptions options)
{
// options.Audience = _azureOptions.ClientId;
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
// The valid audiences are both the Client ID(options.Audience) and api://{ClientID}
// --->>> I've changed this to also have "https://graph.micrososft.com" but no luck
options.TokenValidationParameters.ValidAudiences = new string[] { _azureOptions.ClientId, $"api://{_azureOptions.ClientId}" }; // <<--- I've changed this to "https://graph.micrososft.com" but no luck
// If you want to debug, or just understand the JwtBearer events, uncomment the following line of code
// options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
}
public void Configure(JwtBearerOptions options)
{
Configure(Options.DefaultName, options);
}
}
This is how I gain a token from the MVC project - the authority is the api://client_id:
string userObjectID = User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
//AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority, new NaiveSessionCache(userObjectID, HttpContext.Session));
AuthenticationContext authContext = new AuthenticationContext(AzureAdOptions.Settings.Authority);
ClientCredential credential = new ClientCredential(AzureAdOptions.Settings.ClientId, AzureAdOptions.Settings.ClientSecret);
I appreciate your thoughts and experience on this - thanks again for your time.
Looks like your client app is acquiring a Microsoft Graph API token:
options.Resource = "https://graph.microsoft.com";
An access token has an audience (aud claim) that specifies what API it is meant for.
Your client app needs to use your API's client id or application ID URI as the resource.
This way you get an access token that is meant for your API.
The Resource option there is limited to one API.
If you need tokens for multiple APIs,
you'll need to setup an event listener for AuthorizationCodeReceived and use MSAL.NET to exchange the authorization code for tokens.
I have a sample app that does this: https://github.com/juunas11/aspnetcore2aadauth/blob/97ef0d62297995c350f40515938f7976ab7a9de2/Core2AadAuth/Startup.cs#L58.
This app uses .NET Core 2.2 and ADAL though, but the general approach with MSAL would be similar.
Related
I want to authenticate users in my Web API using the OIDC flow and Google as the ID provider.
In a nutshell, my application is composed of multiple microservices where each is a Web API. The authNZ to the REST endpoints in all the services is through JWT. I have one identity microservice that I want it to implement the OIDC flow, particularly implementing the following three REST endpoints.
login that returns a Challenge (or its URL);
logout endpoint.
callback that is called by Google and should extract user information from the OIDC code (including ID and Access tokens);
Most Microsoft templates for AuthNZ are either mostly built with UI elements or leverage third-party libraries such as Duende, which I cannot use.
I can redirect to Google using the Singin endpoint, though code is null when Google call's back the redirect URI. So, I am not sure what is missing in my configuration.
// Register services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = GoogleDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddGoogle(options =>
{
options.ClientId = "...";
options.ClientSecret = "...";
});
// Configure App
app.UseAuthentication();
app.UseAuthorization();
The controller.
[Route("api/v1/[controller]/[action]")]
[ApiController]
[Authorize]
public class IdentityController : ControllerBase
{
[AllowAnonymous]
[HttpGet]
public IActionResult SignIn()
{
return new ChallengeResult(
"Google",
new AuthenticationProperties
{
IsPersistent = true,
RedirectUri = Url.Action("callback", "Identity")
});
}
[AllowAnonymous]
[HttpGet(Name = "callback")]
public async Task<IActionResult> Callback(object code = null)
{
// code is null here.
}
}
You typically don't use AddGoogle on its own,instead you add AddCookie() as well. So that the application can create a user session cookie.
.AddGoogle(options =>
{
options.ClientId = "...";
options.ClientSecret = "...";
});
See this blog post for more details
https://www.roundthecode.com/dotnet/how-to-add-google-authentication-to-a-asp-net-core-application
Also, make sure you set Cookies as the default authentication scheme
options.DefaultAuthenticateScheme = GoogleDefaults.AuthenticationScheme;
The Callback endpoint should be implemented like the following.
[AllowAnonymous]
[HttpGet(Name = "callback")]
public async Task<IActionResult> callback()
{
var authResult = await HttpContext.AuthenticateAsync(
GoogleDefaults.AuthenticationScheme);
var claims = authResult.Principal.Identities
.FirstOrDefault().Claims.Select(claim => new
{
claim.Issuer,
claim.OriginalIssuer,
claim.Type,
claim.Value
});
return Content(claims.ToString());
}
I have been using cookies to get the authentication results, as suggested by some blogs, as the following, though that did not work for me.
// An incorrect method of getting
// authentication results in my use-case.
var authResult = await HttpContext.AuthenticateAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
I want to implement OAuth in my web application and for that I added the following code in my startup.cs
public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CombiTime API v1.0", Version = "v1" });
c.AddSecurityDefinition("OAuth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("http://localhost:4200/login"),
TokenUrl = new Uri("http://localhost:4200/connect/token")
}
}
});
c.OperationFilter<AuthorizeOperationFilter>();
c.AddSecurityRequirement(new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme{
Reference = new OpenApiReference{
Id = "Bearer", //The name of the previously defined security scheme.
Type = ReferenceType.SecurityScheme
}
},new List<string>()
}
});
});
return services;
}
public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Versioned API v1.0");
c.DocumentTitle = "Title Documentation";
c.DocExpansion(DocExpansion.None);
c.RoutePrefix = string.Empty;
c.OAuthClientId("combitimeapi_swagger");
c.OAuthAppName("Combitime API");
c.OAuthUsePkce();
});
return app;
}
and the AuthorizeOperationFilter Code is as follows :
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Since all the operations in our api are protected, we need not
// check separately if the operation has Authorize attribute
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[
new OpenApiSecurityScheme
{
Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "oauth2"}
}
] = new[] {"combitimeapi"}
}
};
}
By using this code, I get an "Authorize" button on my swagger UI and when I click that button I am redirecting to my login page(front end based on angular). So I gave my AuthorizationUrl as http://localhost:4200/login and then when I am redirected to login page, I login with valid credentials, I have used jwt token for login and for that I added the following code in my startup.cs
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
};
});
I want to redirect back to the swagger UI after I login with valid credentials but the problem is that I am being redirected to the dashboard after I login. Please help me or let me know what I am doing wrong.
The url that is being formed after I am redirected to login page from swagger is :
http://localhost:4200/login?response_type=code&client_id=combitimeapi_swagger&redirect_uri=http:%2F%2Flocalhost:61574%2Foauth2-redirect.html&state=V2VkIEZlYiAxNyAyMDIxIDIyOjU3OjQ2IEdNVCswNTMwIChJbmRpYSBTdGFuZGFyZCBUaW1lKQ%3D%3D&code_challenge=mT0amBTJgczCZmNSZAYVfjzzpaTiGb68XlyR3RNHuas&code_challenge_method=S256
My front-end is running on port 4200.
My swagger is running on port 61574.
But I am not being redirected to swagger UI after putting in valid credentials
Please help me.
First, let me add some details to your picture:
You have two applications, one with API (based on ASP.NET Core) and one with frontend UI (Angular, but it doesn't matter), and, it's important, with authorization/authentication functions.
You use .NETCore 3.1
You configure an authorization for swagger that means any call from swagger UI page will use given authorization parameters.
So, for API application we have to add a class that has helper methods configuring our swagger:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CombiTime API v1.0", Version = "v1" });
c.AddSecurityDefinition(
"oauth2",
new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("https://lvh.me:4201/connect/authorize"),
TokenUrl = new Uri("https://lvh.me:4201/connect/token"),
Scopes = new Dictionary<string, string> {
{ "combitimeapi", "Demo API" }
}
}
}
});
c.OperationFilter<AuthorizeOperationFilter>();
c.AddSecurityRequirement(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme{
Reference = new OpenApiReference{
Id = "oauth2", //The name of the previously defined security scheme.
Type = ReferenceType.SecurityScheme
}
},
new List<string>()
}
});
});
return services;
}
public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Versioned API v1.0");
c.DocumentTitle = "Title Documentation";
c.DocExpansion(DocExpansion.None);
c.RoutePrefix = string.Empty;
c.OAuthClientId("combitimeapi_swagger");
c.OAuthAppName("Combitime API");
c.OAuthScopeSeparator(",");
c.OAuthUsePkce();
});
return app;
}
}
Please, pay attention to the AuthorizationUrl property and to the TokenUrl property. The AuthorizationUrl property should be pointed to our OAuth2 server authorization endpoint. Please, keep in mind that authorization endpoint and logon page are different endpoints. We could get all-known endpoints for our frontend application by visiting the url: https://lvh.me:4201/.well-known/openid-configuration in case our application uses ASP.NET Core with IdentityServer.
Next, Startup.cs of our API application should contain:
public void ConfigureServices(IServiceCollection services)
{
// ... some your code ...
services.AddSwaggerDocumentation();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication("Bearer", options =>
{
options.ApiName = "combitimeapi";
options.Authority = "https://lvh.me:4201";
});
// ... some your code ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... some your code ...
app.UseSwaggerDocumentation();
app.UseRouting();
app.UseAuthorization();
// ... some your code ...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Please, do not forget to add attribute [Authorize] to all your controllers, because your AuthorizeOperationFilter assumes that's done.
Let's look for required changes for our frontend & authorize part. You should configure some certain things, like:
CORS policy
Awailable API clients (one is your Angular UI and another one is API application)
Awailable API resources
Authentication & authorization methods
The class Startup.cs should contain:
public void ConfigureServices(IServiceCollection services)
{
// ... some your code ...
services.AddCors(policies => {
policies.AddDefaultPolicy(builder => {
builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin();
});
});
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.Clients.AddIdentityServerSPA("forntend", cfg => {});
options.Clients.AddNativeApp("combitimeapi_swagger", cfg => {
cfg
.WithRedirectUri("https://lvh.me:5001/oauth2-redirect.html")
.WithScopes("combitimeapi");
});
options.ApiResources.AddApiResource("combitimeapi", cfg => {
cfg.WithScopes("combitimeapi");
});
})
.AddApiResources();
services
.AddAuthentication(
x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddIdentityServerJwt();
// ... some your code ...
}
I use here .AddIdentityServerJwt() instead of your's .AddJwtBearer(...) because I don't have your keys and other specific options.
The frontend application is configured to use ports 4201 for HTTPS and 4200 for HTTP, the API application is configured to use ports 5001 for HTTPS and 5000 for HTTP.
Now you can run both applications and go to the page https://lvh.me:5001/index.html and press the button 'Authorize' to get a dialog like:
Enter you secret, mark scope and press 'Authorize' and, after you authenticate yourself you will get:
If you do not get a successful result, please check log of the frontend application, usually it contains error that could help you to find out a problem.
Hope text above will help you.
If you look at the OAuth Web-site the case is described as Per-Request Customization
Per-Request Customization
Often times a developer will think that they need to be able to use a
different redirect URL on each authorization request, and will try to
change the query string parameters per request. This is not the
intended use of the redirect URL, and should not be allowed by the
authorization server. The server should reject any authorization
requests with redirect URLs that are not an exact match of a
registered URL.
If a client wishes to include request-specific data in the redirect URL, it can > instead use the “state” parameter to store data that will be included after the > user is redirected. It can either encode the data in the state parameter itself, or use the state parameter as a session ID to store the state on the server.
I hope that helps you in your quest.
Source: https://www.oauth.com/oauth2-servers/redirect-uris/redirect-uri-registration/
There may be more than one problem with the Startup code, more properly in the AddSwaggerGen.
Configuration of the Identity Provider:
Independently of the redirect, are you able to get an access token, or are you getting some kind of error, for example in the request or in the Identity Provider itself?
Please note that the client configuration that you provide in Swagger must match the configuration in the Identity Provider. You seem to be following Scott Brady's example; we can observe that all his Swagger's startup configuration follows the information he has in the Identity Server (here).
Set the token in the calls to the API:
Moreover, even if you are getting the token, I think you are not setting it in the subsequent calls from Swagger to the API itself.
The AddSecurityDefinition and the AddSecurityRequirement or the AuthorizeOperationFilter typically mention at least one scheme with the same identifier, since the first method defines the way that Swagger is authenticating and the second/third define the way that the calls to the API are authenticated (so, they must reference each other). However, you are using different IDs in all the three methods - "OAuth2", "Bearer" and "oauth2" -, so none of them is linked.
I don't fully know your application, but I believe you could actually be using only one of the AddSecurityRequirement or the AuthorizeOperationFilter, since they are both specifying security requirements. The most important would be to reference the ID of the SecurityDefinition (in your case, "OAuth2").
Scott's example, in fact, only uses the AuthorizeCheckOperationFilter and uses the same ID for the OpenApiSecurityScheme that was previously registered in the AddSecurityDefinition - in his case, "oauth2", but any name/string could be used.
i've created an API and set up JWT auth from the same API (I chose not to use IdentityServer4).
I did this through services.AddAuthentication
And then I created tokens in the controller and it works.
However I now want to add registration etc. But i prefer not to write my own code for hashing passwords, handling registration emails etc.
So I came across ASP.NET Core Identity and it seems like what I need, except from that it adds some UI stuff that I dont need (because its just an API and the UI i want completely independent).
But on MSDN is written:
ASP.NET Core Identity adds user interface (UI) login functionality to
ASP.NET Core web apps. To secure web APIs and SPAs, use one of the
following:
Azure Active Directory
Azure Active Directory B2C (Azure AD B2C)
IdentityServer4
So is it really a bad idea to use Core Identity just for hashing and registration logic for an API? Cant I just ignore the UI functionality? It's very confusing because I'd rather not use IdentityServer4 or create my own user management logic.
Let me just get off my chest that the bundling Identity does with the UI, the cookies and the confusing various extension methods that add this or that, but don't add this or that, is pretty annoying, at least when you build modern web APIs that need no cookies nor UI.
In some projects I also use manual JWT token generation with Identity for the membership features and user/password management.
Basically the simplest thing to do is to check the source code.
AddDefaultIdentity() adds authentication, adds the Identity cookies, adds the UI, and calls AddIdentityCore(); but has no support for roles:
public static IdentityBuilder AddDefaultIdentity<TUser>(this IServiceCollection services, Action<IdentityOptions> configureOptions) where TUser : class
{
services.AddAuthentication(o =>
{
o.DefaultScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(o => { });
return services.AddIdentityCore<TUser>(o =>
{
o.Stores.MaxLengthForKeys = 128;
configureOptions?.Invoke(o);
})
.AddDefaultUI()
.AddDefaultTokenProviders();
}
AddIdentityCore() is a more stripped down version that only adds basic services, but it doesn't even add authentication, and also no support for roles (here you can already see what individual services are added, to change/override/remove them if you want):
public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services, Action<IdentityOptions> setupAction)
where TUser : class
{
// Services identity depends on
services.AddOptions().AddLogging();
// Services used by identity
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), services);
}
Now that kind of makes sense so far, right?
But enter AddIdentity(), which appears to be the most bloated, the only one that supports roles directly, but confusingly enough it doesn't seem to add the UI:
public static IdentityBuilder AddIdentity<TUser, TRole>(
this IServiceCollection services,
Action<IdentityOptions> setupAction)
where TUser : class
where TRole : class
{
// Services used by identity
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
})
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
};
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
// Hosting doesn't add IHttpContextAccessor by default
services.AddHttpContextAccessor();
// Identity services
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
services.TryAddScoped<SignInManager<TUser>>();
services.TryAddScoped<RoleManager<TRole>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
}
All in all what you probably need is the AddIdentityCore(), plus you have to use AddAuthentication() on your own.
Also, if you use AddIdentity(), be sure to run your AddAuthentication() configuration after calling AddIdentity(), because you have to override the default authentication schemes (I ran into problems related to this, but can't remember the details).
(Another tidbit of information that might be interesting for people reading this is the distinction between SignInManager.PasswordSignInAsync(), SignInManager.CheckPasswordSignInAsync() and UserManager.CheckPasswordAsync(). These are all public methods you can find and call for authorization purposes. PasswordSignInAsync() implements two-factor signin (also sets cookies; probably only when using AddIdentity() or AddDefaultIdentity()) and calls CheckPasswordSignInAsync(), which implements user lockout handling and calls UserManager.CheckPasswordAsync(), which just checks the password. So to get a proper authentication it's better not to call UserManager.CheckPasswordAsync() directly, but to do it through CheckPasswordSignInAsync(). But, in a single-factor JWT token scenario, calling PasswordSignInAsync() is probably not needed (and you can run into redirect issues). If you have included UseAuthentication()/AddAuthentication() in the Startup with the proper JwtBearer token schemes set, then the next time the client sends a request with a valid token attached, the authentication middleware will kick in, and the client will be 'signed in'; i.e. any valid JWT token will allow client to access controller actions protected with [Authorize].)
And IdentityServer is thankfully completely separate from Identity. In fact the decent implementation of IdentityServer is to use it as a standalone literal identity server that issues tokens for your services. But since ASP.NET Core has no token generation capability built-in, a lot of people end up running this bloated server inside their apps just to be able to use JWT tokens, even though they have a single app and they have no real use for a central authority. I don't mean to hate on it, it's a really great solution with a lot of features, but it would be nice to have something simpler for the more common use cases.
You just configure Identity to use JWT bearer tokens. In my case I'm using encrypted token, so depending on your use-case you may want to adjust the configuration:
// In Startup.ConfigureServices...
services.AddDefaultIdentity<ApplicationUser>(
options =>
{
// Configure password options etc.
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure authentication
services.AddAuthentication(
opt =>
{
opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
TokenDecryptionKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key")),
RequireSignedTokens = false, // False because I'm encrypting the token instead
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
// Down in Startup.Configure add authn+authz middlewares
app.UseAuthentication();
app.UseAuthorization();
Then generate a token when the user wants to sign in:
var encKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key"));
var encCreds = new EncryptingCredentials(encKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);
var claimsIdentity = await _claimsIdentiyFactory.CreateAsync(user);
var desc = new SecurityTokenDescriptor
{
Subject = claimsIdentity,
Expires = DateTime.UtcNow.AddMinutes(_configuration.Identity.JwtExpirationMinutes),
Issuer = _configuration.Identity.JwtIssuer,
Audience = _configuration.Identity.JwtAudience,
EncryptingCredentials = encCreds
};
var token = new JwtSecurityTokenHandler().CreateEncodedJwt(desc);
// Return it to the user
You can then use the UserManager to handle creating new users and retrieving users, while SignInManager can be used to check for valid login/credentials before generating the token.
I have two .net core 2.2 projects; the first one is an MVC project which is like presentation layer that user can login and the other Web API that handles DB operations. I want to handle login request in Web API and return the login result to MVC project by using MVC Core Identity. My DbContext is in the Web API project. Is there anyway to create identity cookie according to result of Web API request?
In this situation the authentication should be handled with an access token rather than a cookie.
For your Web API, implement Resource Owner Password Credentials grant type of OAuth2 protocol with the help of IdentityServer 4 library. At the end you should be able to get an access token from the API in exchange of login credentials.
For your MVC project, create a table to store token-sessid pairs, so when the MVC application gets an access token from the API during a session, it will save them in the table. For the subsequent requests the MVC app will get the token from the table (by using the sessid) and use it to access the Web API.
First in the startup class of your MVC on the server side
add->
services.AddAuthentication(options => {
options.DefaultScheme = "Cookies";
}).AddCookie("Cookies", options => {
options.Cookie.Name = "auth_cookie";
options.Cookie.SameSite = SameSiteMode.None;
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
});
Then in your Login Controller of MVC
[HttpPost]
public async Task<IActionResult> Login(string username, string password)
{
if (!IsValidUsernameAndPasswod(username, password))
return BadRequest();
var user = GetUserFromUsername(username);
var claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.Username),
//...
}, "Cookies");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
return NoContent();
}
Notice that we are referencing the “Cookies” authentication scheme we’ve defined in Startup.cs.
Afterwards on your web api->
CookieContainer cookieContainer = new CookieContainer();
HttpClientHandler handler = new HttpClientHandler
{
CookieContainer = cookieContainer
};
handler.CookieContainer = cookieContainer;
var client = new HttpClient(handler);
var loginResponse = await client.PostAsync("http://yourdomain.com/api/account/login?username=theUsername&password=thePassword", null);
if (!loginResponse.IsSuccessStatusCode){
//handle unsuccessful login
}
var authCookie = cookieContainer.GetCookies(new Uri("http://yourdomain.com")).Cast<Cookie>().Single(cookie => cookie.Name == "auth_cookie");
//Save authCookie.ToString() somewhere
//authCookie.ToString() -> auth_cookie=CfDJ8J0_eoL4pK5Hq8bJZ8e1XIXFsDk7xDzvER3g70....
This should help you achieve your Task. Of course change values based on your requirements, but thus code would be a good reference point.
Also add the required code to set cookies from your web api application.
Hope it helps!
I am not sure I’m fully up to speed with your problem statement. I am assuming that you’re after some sort of mechanism that enables a user to pass their identity all the way from web client to your DbContext.
I also assume you then don’t really authenticate users on the MVC app and basically proxy their requests onto WebAPI.
If so, you might want to consider crafting a JWT (preferably, signed) token as your WebAPI response and then storing it on the client (I guess cookie is a good enough mechanism).
Then MVC project will naturally get the token by virtue of Session State and all you will have to do would be to pass it along with every WebAPI request you make.
1. Only two services
If your system has only two services (front and back) you could use Cookies for all your authentication schemes in your front and consume your api for user validation.
Implement the login page in your web app and verify the users from your login action method (post) calling a backend endpoint (your api) where you can validate the credentials against your database. Note that you do not need to publish this endpoint in internet.
ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/auth/login";
options.LogoutPath = "/auth/logout";
});
AuthController:
[HttpPost]
public IActionResult Login([FromBody] LoginViewModel loginViewModel)
{
User user = authenticationService.ValidateUserCredentials(loginViewModel.Username, loginViewModel.Password);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Role, user.Role),
new Claim(ClaimTypes.Email, user.Email)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(principal);
return Redirect(loginViewModel.ReturnUrl);
}
ModelState.AddModelError("LoginError", "Invalid credentials");
return View(loginViewModel);
}
2. OAuth2 or OpenId Connect dedicated server
But if you want to implement your own authorization service or idenitity provider which can be used by all your applications (both front and back) I would recommend creating your own server with a standard like OAuth2 or OpenId. This service should be dedicated exclusively for this purpose.
If your services are net core you could use IdentityServer. It is a middleware that is certified by OpenIdConnect and is very complete and extensible. You have extensive documentation and it's easy to implement for both OAuth2 and OpenId. You could add your dbContext to use your user model.
Your Web app ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "https://myauthority.com";
options.ClientId = "client";
options.ClientSecret = "secret";
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("myapi");
// ...
}
Your Identity Provider ConfigureServices:
services.AddIdentityServer()
.AddInMemoryClients(Config.GetClients())
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddDeveloperSigningCredential();
In this way your fronts would request access to the scopes necessary to access the apis. The front would receive an access token with this scope (if this client is allowed for the requested scopes). These apis in turn could validate the access token with a validation middleware like the following:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://myauthority.com";
options.Audience = "myapi";
});
3. Custom remote handler
If you still prefer to implement something remote by yourself you can implement your custom RemoteAuthenticationHandler. This abstract class helps you to redirect to a remote login service (your api) and handles results on callback redirections with the authorization result in your web app. This result is used to populate the user ClaimsPrincipal and if you configure your web app authentication services in this way you could maintain the user session in a Cookie:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "CustomScheme";
})
.AddCookie()
.AddRemoteScheme<CustomRemoteAuthenticationOptions, CustomRemoteAuthenticationHandler>("CustomScheme", "Custom", options =>
{
options.AuthorizationEndpoint = "https://myapi.com/authorize";
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.SaveTokens = true;
options.CallbackPath = "/mycallback";
});
You can see the remote handlers OAuthHandler or OpenIdConnectHandler as a guide to implement yours.
Implementing your own handler (and handler options) could be cumbersome and insecure, so you should to consider the first options.
In my .net core 1.1 code I'm doing the authentication as follows(which is sending the bearer token to external URL and look for claims in the return token). This code is in Configure method
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = "Cookies",
Authority = signinAuthority,
RequireHttpsMetadata = signinHTTPS,
ClientId = "skybus",
ClientSecret = "secret",
ResponseType = "code id_token",
Scope = { "api1", "offline_access" },
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true
});
Now I upgraded my code to .net Core 2.0 the both UseCookieAuthentication & UseOpenIdConnectAuthentication are changed. I'm finding it difficult to find what needs to be done in this case
What I changed it to is as follows in ConfigureServices method
services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(o =>
{
o.Authority = signinAuthority;
o.SignInScheme = "Cookies";
o.RequireHttpsMetadata = signinHTTPS;
o.ClientId = "skybus";
o.ClientSecret = "secret";
o.ResponseType = "code id_token";
o.GetClaimsFromUserInfoEndpoint = true;
o.SaveTokens = true;
o.Scope.Add("api1");
o.Scope.Add("offline_access");
});
In Browser I see this URL after the above changes. It should either show me the external login page if user is not logged in or return to home page of my website
http://localhost:5000/Account/Login?ReturnUrl=%2F
I followed this link from microsoft to start my migration. Most of the migration is covered by the this link, but I faced issue where most of my claims are missing.
With the ASP.NET Core 1.x, client would have received the claims: nbf, exp, iss, aud, nonce, iat, c_hash, sid, sub, auth_time, idp, amr.
In Core 2.0 we only get sid, sub and idp. What happened?
Microsoft added a new concept to their OpenID Connect handler called ClaimActions. Claim actions allow modifying how claims from an external provider are mapped (or not) to a claim in your ClaimsPrincipal. Looking at the ctor of the OpenIdConnectOptions, you can see that the handler will now skip the following claims by default:
ClaimActions.DeleteClaim("nonce");
ClaimActions.DeleteClaim("aud");
ClaimActions.DeleteClaim("azp");
ClaimActions.DeleteClaim("acr");
ClaimActions.DeleteClaim("amr");
ClaimActions.DeleteClaim("iss");
ClaimActions.DeleteClaim("iat");
ClaimActions.DeleteClaim("nbf");
ClaimActions.DeleteClaim("exp");
ClaimActions.DeleteClaim("at_hash");
ClaimActions.DeleteClaim("c_hash");
ClaimActions.DeleteClaim("auth_time");
ClaimActions.DeleteClaim("ipaddr");
ClaimActions.DeleteClaim("platf");
ClaimActions.DeleteClaim("ver");
If you want to “un-skip” a claim, you need to delete a specific claim action when setting up the handler. The following is the very intuitive syntax to get the amr claim back:
options.ClaimActions.Remove("amr");
Requesting more claims from the OIDC provider
When you are requesting more scopes, e.g. profile or custom scopes that result in more claims, there is another confusing detail to be aware of.
Depending on the response_type in the OIDC protocol, some claims are transferred via the id_token and some via the userinfo endpoint.
So first of all, you need to enable support for the userinfo endpoint in the handler:
options.GetClaimsFromUserInfoEndpoint = true;
In the end you need to add the following class to import all other custom claims
public class MapAllClaimsAction : ClaimAction
{
public MapAllClaimsAction() : base(string.Empty, string.Empty)
{
}
public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
{
foreach (var claim in identity.Claims)
{
// If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set
var shortClaimTypeName = claim.Properties.ContainsKey(JwtSecurityTokenHandler.ShortClaimTypeProperty) ?
claim.Properties[JwtSecurityTokenHandler.ShortClaimTypeProperty] : string.Empty;
// checking if claim in the identity (generated from id_token) has the same type as a claim retrieved from userinfo endpoint
JToken value;
var isClaimIncluded = userData.TryGetValue(claim.Type, out value) || userData.TryGetValue(shortClaimTypeName, out value);
// if a same claim exists (matching both type and value) both in id_token identity and userinfo response, remove the json entry from the userinfo response
if (isClaimIncluded && claim.Value.Equals(value.ToString(), StringComparison.Ordinal))
{
if (!userData.Remove(claim.Type))
{
userData.Remove(shortClaimTypeName);
}
}
}
// adding remaining unique claims from userinfo endpoint to the identity
foreach (var pair in userData)
{
JToken value;
var claimValue = userData.TryGetValue(pair.Key, out value) ? value.ToString() : null;
identity.AddClaim(new Claim(pair.Key, claimValue, ClaimValueTypes.String, issuer));
}
}
}
and then use the above class code to add it to ClaimActions
options.ClaimActions.Add(new MapAllClaimsAction());
I don't know if you are using IdentityServer4 but they have provided some samples on github for ASP.NET Core 2.0.
Hope this helps you.
Sample Project
Did you take the authentication middleware into use?
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
...