I implemented most of IdentityServer4 and .NET Core 2 following the Quickstarts and it works well.
However I wish to separate the access from the application and to the API running on the same web. I have an application and an API running on the same web and I want to give a secure access to this API while the application also needs a login.
So on the web site, I want users to be able to access the application as well as the API.
However for external applications which should only access the API, I wish they cannot access the rest of the application.
I understand I could do this by having 2 different webs: one for the application and one for the API but I wish to have them both on the same URL.
So I think I simply need 2 different clients connected to the same users database (using ASP.NET Identity):
new Client
{
ClientId = "sapi",
ClientName = "Secure API",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = true,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes = { "sapi" }
},
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"sapi"
},
AllowOfflineAccess = true
}
So the idea is that the client mvc can access the application and API, while sapi client can only access the API and not the application. Any user (stored in ASP.NET Identity) can be used with client mvc or sapi.
The problem is how do I tell if the user has been authenticated using client mvc or using client sapi?
When the user has logged in, in the claims you will have one claim named aud. It's value should be the clientId. Then you can have some custom authorization to either reject or allow authorization to the application.
Related
I have a Blazor WASM client application that is trying calling an aspnet web api. From the web API, I am trying to use the Microsoft Graph SDK with delegated permissions.
On the server, when I retrieve an instance of GraphServiceClient through dependency injection, I get the following error when trying to read user data.
var me = await graph.Me.Request().GetAsync();
StatusCode: 400
ResponseBody: {
"error":"invalid_grant",
"error_description":"AADSTS70000: The request was denied because one or more scopes requested are unauthorized or expired. The user must first sign in and grant the client application access to the requested scope.",
"error_codes":[70000],
"error_uri":"https://login.microsoftonline.com/error?code=70000",
"suberror":"consent_required",
(some data omitted)
}
However, if I obtain a token manually with ITokenAcquisition, everything works correctly.
var token = await tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "User.Read" });
GraphServiceClient graph = new GraphServiceClient(new DelegateAuthenticationProvider(request =>
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
return Task.CompletedTask;
}));
var me = await graph.Me.Request().GetAsync();
I was under the impression that the injected GraphServiceClient would obtain tokens using the ITokenAcquisition and passing the scopes specified in Startup.
Am I configuring something wrong? It seems that the token used in the injected client doesn;t contain the proper scopes.
I have tried fully logging out of my Microsoft account, and deleting the browser cache. I have consented to all of the necessary permissions.
My api server startup looks like this.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(builder.Configuration.GetSection("GraphAPI"))
.AddInMemoryTokenCaches();
server appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "consumers",
"ClientId": "{clientId}",
"ClientSecret": "{clientSecret}",
"Scopes": "AccessAsUser",
"CallbackPath": "/signin-oidc"
},
"GraphAPI": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "User.Read"
},
Client startup
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.LoginMode = "redirect";
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://{clientId}/.default");
options.ProviderOptions.DefaultAccessTokenScopes.Add("profile");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
});
client appsettings.json
"AzureAd": {
"Authority": "https://login.microsoftonline.com/consumers",
"ClientId": "{clientId}",
"ValidateAuthority": true
}
What happens if you try:
var me = await graph.Me.Request().WithScopes("https://graph.microsoft.com/.default").GetAsync();
Using the DI injected GraphServiceClient instance?
It states here
that going forward, we must prefer the auth code flow method since "With the plans for third party cookies to be removed from browsers, the implicit grant flow is no longer a suitable authentication method."
I have set up an asp.net mvc 4 web application according to the sample app here. It works when I set up my app registration in AD B2C directory for Access Tokens (used for implicit grant flows). If I switch to "ID Tokens (for implicit and hybrid flows)" as the documentation recommends, I get error that my application is not setup for it.
As I understand from the documentation, I would have to specifiy separate endpoins for /authorize and/token to fetch a token after authorization. I am not sure from looking at the sample though how exactly I can do this. Below is the ConfigureAuth method as you can see in the sample code on github link provided:
public void ConfigureAuth(IAppBuilder app)
{
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(WellKnownMetadata, Tenant, DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}",
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebCookieManager()
}
);
}
I am developing an app with .NET Core Web API, Entity Framework and React. I've been reading a lot recently about possible authentication techniques for my API and I've discovered that plain JWT is not entirely secure, so at first I decided to use OpenID Connect with IdentityServer 4. I understand the idea behind OAuth 2.0 and OpenID Connect is to hide user credentials during login process and to involve external authentication provider in issuing an access token, but I don't want to rely on such services because not everyone have an account on Facebook etc. I consider this as an optional way to login. I want to give users an ability to sign in with just login and password. So what is the best (secure) way to accomplish this in modern web apps?
Having project 1 as Client App, project 2 as API Resources and project 3 as Authorization Service (IdentityServer4), I consider following scenarios:
A user is able to create an account on Authorization Service which is responsible for issuing a token required to get access to API Resources through Client App. Authorization Service is registered as authorization provider only for my Client App.
Get authorization token from Authorization Service using resource owner password grant - this one is not recommended by the specs but in my case since user must provide credentials to Authorization Service anyway and I will be hosting every project I can't see any problem.
Don't bother with OAuth and implement authorization mechanism using ASP.NET Core Identity + bearer token authentication.
Any ideas or recommendations highly apprecieated.
I use the JwtBearer package, wire it up in your Startup.cs Configure method like
.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["AppSettings:AuthConfig:SecretKey"])),
ValidateIssuer = true,
ValidIssuer = Configuration["AppSettings:AuthConfig:Issuer"],
ValidateAudience = true,
ValidAudience = Configuration["AppSettings:AuthConfig:Audience"],
ValidateLifetime = true,
}
})
and my login action on my User controller looks like
[HttpPost]
public string Post([FromBody]LoginRequest request)
{
var contact = dbContext.Contacts.Where(c => c.Active && c.Email == request.Email).Select(c => new { c.Id, c.PasswordHash }).SingleOrDefault();
if (contact == null || !Security.PasswordHash.ValidatePassword(request.Password, contact.PasswordHash))
{
return string.Empty;
}
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(appSettings.AuthConfig.SecretKey));
var now = DateTime.UtcNow;
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, contact.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
};
var jwt = new JwtSecurityToken(
issuer: appSettings.AuthConfig.Issuer,
audience: appSettings.AuthConfig.Audience,
claims: claims,
notBefore: now,
expires: now.AddDays(30),
signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256));
jwt.Payload.Add("roles", dbContext.ContactRoles.Where(cr => cr.ContactId == contact.Id).Select(ur => ur.Role.Name).ToArray());
return new JwtSecurityTokenHandler().WriteToken(jwt);
}
I use a JWT package for Angular on the client, there may be something similar for React.
I've created a backend WebApi to create JWT tokens and they're working fine when I use PostMan to access restricted resources by adding the token to the header, e.g. [Authorise(Roles="SuperAdmin")].
I want to use this infrastructure with my MVC app, but don't quite know how to tie it together.
I'm guessing that when the user creates an account and I generate a JWT for them (Via the WebApi), I need to stick the token in a cookie, but how do this and also extract the JWT from the cookie on future requests, so that it will work with the normal [Authorize] attribute that I decorate the ActionResults with?
Do I need to put something in the Owin pipeline?
Or do I need to create a custom [Authorize] attribute?
My Startup.cs file currently looks like this:
public void Configuration(IAppBuilder app)
{
HttpConfiguration httpConfig = new HttpConfiguration();
ConfigureOAuthTokenGeneration(app);
ConfigureOAuthTokenConsumption(app);
ConfigureWebApi(httpConfig);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(httpConfig);
}
private void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//TODO: enforce https in live
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new CustomOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat("https://localhost:443")
};
// Plugin the OAuth bearer JSON Web Token tokens generation and Consumption will be here
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
private void ConfigureOAuthTokenConsumption(IAppBuilder app)
{
var issuer = "https://localhost:443";
string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
byte[] audienceSecret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["as:AudienceSecret"]);
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
}
private void ConfigureWebApi(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
If it helps, I was following this guide:
http://bitoftech.net/2015/02/16/implement-oauth-json-web-tokens-authentication-in-asp-net-web-api-and-identity-2/
The infrastructure your referred to is really designed to handle direct web API calls. A classic redirect based web app would fall back on more traditional patterns, where the app receives one token, validates it and uses it to initiate an authenticated session (by saving the results of the token validation in some session artifact, like a token). Although you can implement this patters starting form any token based system, including your custom one, usually it's more convenient (and secure) to leverage existing protocols (like OpenId Connect) and existing products (like Azure AD or Identity Server). See this for a simple example based on Azure AD - the middleware remains the same no matter what OpenId Provider you pick.
I'm trying to use the GMail API in an MVC 5 project, but I seem to having difficulties on how to achieve that using the Owin Middleware for Authentication
I'm able to login via a Google account, and I can also get the user token as such
var googleOptions = new GoogleOAuth2AuthenticationOptions
{
ClientId = "xxx",
ClientSecret = "yyy",
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = async ctx =>
{
ctx.Identity.AddClaim(new Claim("urn:tokens:google:accesstoken", ctx.AccessToken));
}
},
};
app.UseGoogleAuthentication(googleOptions);
I get the access token as I would expect, but Google Quicktart Tutorial found here seems to suggest a very different way to accomplish the Authentication
Is there a way I can use this AccessToken to create the objects required in the tutorial ?
Or are these two completely different?