auth0 authorisation in asp.net core app (MVC) - asp.net-core-mvc

I'am trying to get Auth0 to work in my MVC app.
Although the authentication is working, I cannot seem to get the authorization working.
I've followed this tutorial: https://auth0.com/docs/quickstart/webapp/aspnet-core
my code:
public static IServiceCollection AddAuth0(this IServiceCollection services, IConfiguration configuration)
{
var auth0Options = configuration.GetSection(nameof(Auth0Config))
.Get<Auth0Config>();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
// options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}) //do i need this for access_token?
// .AddJwtBearer(options =>
// {
// options.Authority = auth0Options.Domain;
// options.Audience = auth0Options.ApiIdentifier;
//
// options.SaveToken = true;
// options.RequireHttpsMetadata = false;
// })
.AddCookie()
.AddOpenIdConnect(Auth0Constants.Auth0Scheme, options =>
{
options.Authority = $"https://{auth0Options.Domain}";
options.ClientId = auth0Options.ClientId;
options.ClientSecret = auth0Options.ClientSecret;
options.ResponseType = Auth0Constants.ResponseTypeCode;
options.SaveToken = true;
options.Scope.Clear();
options.Scope.Add(Auth0Constants.Auth0Scope.openid.ToString());
options.Scope.Add(Auth0Constants.Auth0Scope.email.ToString());
options.Scope.Add(Auth0Constants.Auth0Scope.profile.ToString());
options.Scope.Add("read:cars");
options.CallbackPath = new PathString("/callback");
options.ClaimsIssuer = Auth0Constants.Auth0Scheme;
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = context => OnRedirectToIdentityProviderForSignOut(context, auth0Options),
OnRedirectToIdentityProvider = context => OnRedirectToIdentityProvider(context, auth0Options)
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("read:cars", policy => policy.Requirements.Add(new HasScopeRequirement("read:cars", auth0Options.Domain)));
});
services.AddSingleton<IAuthorizationHandler, HasScopeHandler>();
return services;
}
private static Task OnRedirectToIdentityProvider(RedirectContext context, Auth0Config config)
{
context.ProtocolMessage.SetParameter("audience", config.ApiIdentifier);
return Task.CompletedTask;
}
private static Task OnRedirectToIdentityProviderForSignOut(RedirectContext context, Auth0Config auth0Options)
{
var logoutUri = $"https://{auth0Options.Domain}/v2/logout?client_id={auth0Options.ClientId}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
When i look at my charles session, i do see the correct scopes and permissions coming back in the token:
"scope": "openid profile email read:cars",
"permissions": [
"read:cars"
]
But for instance, I cannot get the access_token like they say I can:
var accessToken = await HttpContext.GetTokenAsync("access_token");
this returns null; It is also not in the claims.
On one of my controllers I have: [Authorize("read:cars")] And I get an access denied, do i remove that permission and only use the Authorize, then I am good.
To check if the scope is present:
public class HasScopeRequirement : IAuthorizationRequirement
{
public string Issuer { get; }
public string Scope { get; }
public HasScopeRequirement(string scope, string issuer)
{
Scope = scope;
Issuer = issuer;
}
}
public class HasScopeHandler : AuthorizationHandler<HasScopeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
{
// If user does not have the scope claim, get out of here
if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer))
return Task.CompletedTask;
// Split the scopes string into an array
var scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer).Value.Split(' ');
// Succeed if the scope array contains the required scope
if (scopes.Any(s => s == requirement.Scope))
context.Succeed(requirement);
return Task.CompletedTask;
}
}
But I guess this is not my problem, I think it lies within my access_token that i cannot even to read. So I am thinking that I am missing something. Is it due to the DefaultAuthenticationScheme/ChallengeScheme or ...?

thnx to Kirk!
fixed a bit different then my original idea; now i am using roles.
in auth0 I have users, and assign them the desired roles;
and as a rule:
function (user, context, callback) {
context.idToken['http://schemas.microsoft.com/ws/2008/06/identity/claims/roles'] = context.authorization.roles;
callback(null, user, context);
}
I am using http://schemas.microsoft.com/ws/2008/06/identity/claims/roles as a default claim, because .net core maps to that by default.
in my setup I've added:
options.TokenValidationParameters= new TokenValidationParameters
{
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/roles"
};
Now i can use the Authorize attribute on my controllers
just to be clear; I only have one webapp.If I would go to a different API then I would have to use the access_token off course.

Related

ASP.NET Web API work cors with identity server 4

I try to connect the ASP.NET Web API (not .NET Core) with identity server.
I use owin as startup but I get a cors error.
Why does cors
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
not work with UseOpenIdConnectAuthentication? It works when I remove it
public class Startup1
{
public void Configuration(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "insig_spa",
Authority = "https://localhost:5000",
RedirectUri = "https://localhost:5002/auth-callback",
Scope = "openid profile email insigapi.read",
SignInAsAuthenticationType = "cookie",
RequireHttpsMetadata = false,
UseTokenLifetime = false,
RedeemCode = true,
SaveTokens = true,
ClientSecret = "secret",
ResponseType = OpenIdConnectResponseType.Code,
ResponseMode = "query",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
// set PKCE parameters
var codeVerifier = CryptoRandom.CreateUniqueId(32);
string codeChallenge;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
codeChallenge = Base64Url.Encode(challengeBytes);
}
n.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
n.ProtocolMessage.SetParameter("code_challenge_method", "S256");
// remember code_verifier (adapted from OWIN nonce cookie)
RememberCodeVerifier(n, codeVerifier);
}
return Task.CompletedTask;
},
AuthorizationCodeReceived = n =>
{
// get code_verifier
var codeVerifier = RetrieveCodeVerifier(n);
// attach code_verifier
n.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);
return Task.CompletedTask;
}
}
});
}
}

IdentityServer 4 - Adding a custom claim to a User when using Implicit grant type

I'm going through the tutorial of IdentityServer 4 where is explained how to add user authentication with OpenID Connect, it can be found here:
http://docs.identityserver.io/en/latest/quickstarts/3_interactive_login.html
Basically in this tutorial we have an MVC application with a controller action decorated with a Authorized attribute.
Each time a user tries to access that action, in case he/she is not logged in, the MVC application redirects the user to Identity Server so he/she can input the login credentials.
If the credentials are correct, Identity Server redirects back to the MVC application where a page with the User's credentials is shown.
I've concluded the tutorial and now I want to explore a bit more by adding new claims to the token but I haven't been successful so far.
In the tutorial, the scopes OpenId and Profile are added by setting the AllowedScopes on the Client configuration.
I tried to do create a "age" scope and add it in the same manner, but it didn't work.
Does anyone have an idea how I can do this?
The code is shown bellow (commented lines are stuff I already tried).
IdentityServer setup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
}
In memory stores config:
public class Config
{
public static IEnumerable<Client> GetClients()
{
return new List<Client>()
{
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
RequireConsent = false,
//AlwaysIncludeUserClaimsInIdToken = true,
//AlwaysSendClientClaims = true,
//AllowAccessTokensViaBrowser = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"age"
}
}
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>()
{
new TestUser()
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim>()
{
new Claim("age", "15"),
new Claim("name", "Alice"),
new Claim("website", "https://alice.com")
}
},
new TestUser()
{
SubjectId = "2",
Username = "bob",
Password = "password",
Claims = new List<Claim>()
{
new Claim("age", "16"),
new Claim("name", "Bob"),
new Claim("website", "https://bob.com")
}
}
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource()
{
DisplayName = "Age",
Name = "age",
UserClaims = new List<string>()
{
"age"
}
}
};
}
}
MVC Application Config:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddRazorViewEngine()
.AddAuthorization()
.AddJsonFormatters();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.SaveTokens = true;
});
}
MVC Application Claims Page:
<dl>
#foreach (var claim in User.Claims)
{
<dt>#claim.Type</dt>
<dd>#claim.Value</dd>
}
</dl>
This is the result after a successful login:
sid
ba7ecb47f66524acce04e321b8d2c444
sub
2
idp
local
name
Bob
website
https://bob.com
As you can see the profile claims (name and website) show up, but the custom "age" claim does not.
The answer to the original question is to explicitly add which claims we want to use when setting up OpenId Connect.
We neet to add the following lines inside the .AddOpenIdConnect method:
options.Scope.Clear();
options.Scope.Add("age");
The complete Setup is shown below:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddRazorViewEngine()
.AddAuthorization()
.AddJsonFormatters();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("age");
//Add all the claims you need
});
}

IdentityServer4 Net Core 2 not calling custom iProfileService

I've upgraded my Identity Server project to Net Core 2 and now I am not able to get the iProfileService object to be called to add in custom user claims. It did work in Net Core 1.
Startup.cs ConfigureServices function
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddTransient<IProfileService, M25ProfileService>();
//Load certificate
var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath,
"m25id-cert.pfx"), "mypassword");
services.AddIdentityServer()
.AddSigningCredential(cert)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
//options.EnableTokenCleanup = true;
//options.TokenCleanupInterval = 30;
})
.AddProfileService<M25ProfileService>();
.AddAspNetIdentity<ApplicationUser>();
M25ProfileService.cs
public class M25ProfileService : IProfileService
{
public M25ProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.GivenName, user.FirstName),
new Claim(JwtClaimTypes.FamilyName, user.LastName),
new Claim(IdentityServerConstants.StandardScopes.Email, user.Email),
new Claim("uid", user.Id),
new Claim(JwtClaimTypes.ZoneInfo, user.TimeZone)
};
if (user.UserType != null)
claims.Add(new Claim("mut", ((int)user.UserType).ToString()));
context.IssuedClaims.AddRange(claims);
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
context.IsActive = user != null;
return Task.FromResult(0);
}
}
}
Config.cs
public class Config
{
// try adding claims to id token
public static IEnumerable<IdentityResource> GetIdentityResources()
{
var m25Profile = new IdentityResource(
"m25.profile",
"m25 Profile",
new[]
{
ClaimTypes.Name,
ClaimTypes.Email,
IdentityServerConstants.StandardScopes.OpenId,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,
IdentityServerConstants.StandardScopes.Email,
"uid",
JwtClaimTypes.ZoneInfo
}
);
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
m25Profile
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
//Try adding claims to access token
return new List<ApiResource>
{
new ApiResource(
"m25api",
"message25 API",
new[]
{
ClaimTypes.Name,
ClaimTypes.Email,
IdentityServerConstants.StandardScopes.OpenId,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,
IdentityServerConstants.StandardScopes.Email,
"uid",
JwtClaimTypes.ZoneInfo
}
)
};
}
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
ClientName = "Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"m25api"
}
},
// Local Development Client
new Client
{
ClientId = "m25AppDev",
ClientName = "me25",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
RedirectUris = { "http://localhost:4200/authorize.html" },
PostLogoutRedirectUris = { "http://localhost:4200/index.html" },
AllowedCorsOrigins = { "http://localhost:4200" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
JwtClaimTypes.GivenName,
"mut",
"m25api"
},
AllowOfflineAccess = true,
IdentityTokenLifetime = 300,
AccessTokenLifetime = 86400
}
};
}
}
The first thing I'm trying is just to get the identity server to allow me to login and show the user claims similar to the id4 samples. When I login, the standard claims are listed but none of the custom claims. I've put break points in the M25ProfileService class but they never get hit. It seems that ID4 is never using the customer ProfileService class but I do have it in my startup.cs.
I've also tried from my test JS Client and get the same results. Here's a snippet from my JS Client:
var config = {
authority: "http://localhost:5000",
client_id: "m25AppDev",
redirect_uri: "http://localhost:4200/authorize.html",
response_type: "id_token token",
scope:"openid profile m25api",
post_logout_redirect_uri : "http://localhost:4200/index.html"
};
var mgr = new Oidc.UserManager(config);
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
document.getElementById("accessToken").innerHTML = "Bearer " + user.access_token + "\r\n";
}
else {
log("User not logged in");
}
});
function login() {
mgr.signinRedirect();
}
At this point, I'm not sure what to try. I thought if I added the claims to the id token (GetIdentityResources() function from what I understand) and even the access token (GetApiResources() function from what I understand), I'd see the claims but nothing seems to work. Please help! Thanks in advance!
Also, I used to be able to get the custom claims from my client as well as from the Identity Server's own index page that renders after log
Change the order of these lines of code:
.AddProfileService<M25ProfileService>()
.AddAspNetIdentity<ApplicationUser>();
One if overwriting the other.
I figured it out. Thanks to some code on GitHub, I was able to figure out what I was missing. I just needed to add these 2 lines to each client's config in config.cs and all worked perfect!
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true
This works for remote clients. However, I still can't get it to work when I'm on the ID Server itself logging in (not from a client). That's not a big deal for now but could be something in the future. If/When I figure that piece out, I'll try to remember to update my answer. Meanwhile, I hope this helps others.
In addition to the answers above (and beside the fact that the Startup.cs shown in the question already contained the relevant line of code) I'd like to add another, yet very simple cause for why the Profile Service might not be called:
Don't forget to register the service with the dependency injection container!
As having just .AddProfileService<ProfileService>() is not enough.
You would also need:
services.AddScoped<IProfileService, ProfileService>();
Or:
services.AddTransient<IProfileService, ProfileService>();

IdentityServer4 on Core 2.0 Getting 401 (Unauthorized) from the API

I'm all of a sudden getting 401 errors from my web API project after trying to migrate to the latest version of IdentityServer4 and .NET Core 2. Over the last week, I've made so many changes, I don't know what's right or wrong anymore.
I have a small 3 project solution:
IDSRV: https://localhost:44300
WEB App: https://localhost:44301
API App: https://localhost:44302
Here is my Code to configure IDSV which is stored in my database:
public class Config
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource(){
Name = "api.oc.com",
Description = "OC Api",
Scopes = new[] {new Scope("api.oc.com")}
}
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "oc.com",
ClientName = "OC Website",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AlwaysIncludeUserClaimsInIdToken = true,
ClientSecrets =
{
new Secret("SomeReallyStrongPassword1!".Sha256())
},
RedirectUris = { "https://localhost:44301/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44301/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api.oc.com",
"offline_access"
},
AllowOfflineAccess = true
}
};
}
}
In my WEB API Startup.cs. I have this in ConfigureServices. I believe my Configure method is ok.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = "https://localhost:44300";
o.RequireHttpsMetadata = false;
o.Audience = "api.oc.com";
});
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
This brings me to the WEB Front End trying to authenticate to the WEB API.
I've tried it both with Access Tokens & ID Tokens, but none seem to work.
With Access Token:
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.SetBearerToken(accessToken);
var result = await client.GetStringAsync("https://localhost:44302/api/text/welcome");
Or with the client credentials flow:
var disco = await DiscoveryClient.GetAsync("https://localhost:44300");
var tokenClient = new TokenClient(disco.TokenEndpoint, "oc.com", "SomeReallyStrongPassword1!");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync();
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var result = await client.GetStringAsync("https://localhost:44302/api/text/welcome");
I'm sincerely thankful if anyone has any incite on this. It's been so long looking at security code that my eyeballs are going explode! LOL.
Thanks,
Mike Kushner
After some meddling.. And adding a real certificate, this seems to be working after I created some policies instead of just using the [Authorize] attribute only. Note, that I'm also using IdentityServer4.AccessTokenValidation again.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization((options) => {
options.AddPolicy("MustBeValidUser", policybuilder =>
{
policybuilder.RequireAuthenticatedUser();
policybuilder.Requirements = new[] { new MustBeValidUserRequirement() };
});
});
services.AddSingleton<IAuthorizationHandler, MustBeValidUserHandler>();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:44300";
options.RequireHttpsMetadata = true;
options.ApiName = "api.oc.com";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseDeveloperExceptionPage();
app.UseAuthentication();
app.UseMvc();
}
}

web api - asp.net identity token expires even for the subsequent request

I am using asp.net identity for the token based authentication in web api.
For refresh token, I've implemented based on the following link
http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/
I've added the following two classes and mentioned in the start up configuration.
From the ui I've called with username and password alone through the api
http://domain/token
When I call the above api, the request directly goes to the method ValidateClientAuthentication.
But in this method the logic is, we need to send the client id and client secret.
How do we know these two before the user login for the specific user?
I thought the work flow should be like, we need to check username and password against database and should generate the access token and refresh token.
But here where do i do this logic.
What is the work flow of this system mentioned in the sample?
Before this system, I'll call the Common/login api in my application, and after successful verification,
I'll call the code to make the user as logged in
var userIdentity=await user.GenerateUserIdentityAsync(UserManager);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, userIdentity);
After the above code, I'll generate the access token from the user identity.
I've tried many times with the following implementation and fed up with the flow.
Help me regarding the logic and the flow mentioned here.
SimpleAuthorizationServerProvider
namespace AngularJSAuthentication.API.Providers
{
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId = string.Empty;
string clientSecret = string.Empty;
Client client = null;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (context.ClientId == null)
{
//Remove the comments from the below line context.SetError, and invalidate context
//if you want to force sending clientId/secrects once obtain access tokens.
context.Validated();
//context.SetError("invalid_clientId", "ClientId should be sent.");
return Task.FromResult<object>(null);
}
using (AuthRepository _repo = new AuthRepository())
{
client = _repo.FindClient(context.ClientId);
}
if (client == null)
{
context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId));
return Task.FromResult<object>(null);
}
if (client.ApplicationType == Models.ApplicationTypes.NativeConfidential)
{
if (string.IsNullOrWhiteSpace(clientSecret))
{
context.SetError("invalid_clientId", "Client secret should be sent.");
return Task.FromResult<object>(null);
}
else
{
if (client.Secret != Helper.GetHash(clientSecret))
{
context.SetError("invalid_clientId", "Client secret is invalid.");
return Task.FromResult<object>(null);
}
}
}
if (!client.Active)
{
context.SetError("invalid_clientId", "Client is inactive.");
return Task.FromResult<object>(null);
}
context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());
context.Validated();
return Task.FromResult<object>(null);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
if (allowedOrigin == null) allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
using (AuthRepository _repo = new AuthRepository())
{
IdentityUser user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
identity.AddClaim(new Claim("sub", context.UserName));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
},
{
"userName", context.UserName
}
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
}
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId;
if (originalClient != currentClient)
{
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
return Task.FromResult<object>(null);
}
// Change auth ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
var newClaim = newIdentity.Claims.Where(c => c.Type == "newClaim").FirstOrDefault();
if (newClaim != null)
{
newIdentity.RemoveClaim(newClaim);
}
newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
}
}
SimpleRefreshTokenProvider
namespace AngularJSAuthentication.API.Providers
{
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var clientid = context.Ticket.Properties.Dictionary["as:client_id"];
if (string.IsNullOrEmpty(clientid))
{
return;
}
var refreshTokenId = Guid.NewGuid().ToString("n");
using (AuthRepository _repo = new AuthRepository())
{
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
var token = new RefreshToken()
{
Id = Helper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;
token.ProtectedTicket = context.SerializeTicket();
var result = await _repo.AddRefreshToken(token);
if (result)
{
context.SetToken(refreshTokenId);
}
}
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
string hashedTokenId = Helper.GetHash(context.Token);
using (AuthRepository _repo = new AuthRepository())
{
var refreshToken = await _repo.FindRefreshToken(hashedTokenId);
if (refreshToken != null )
{
//Get protectedTicket from refreshToken class
context.DeserializeTicket(refreshToken.ProtectedTicket);
var result = await _repo.RemoveRefreshToken(hashedTokenId);
}
}
}
public void Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
}
public void Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
}
}
}
How about using refresh tokens and storing them in a database, like in these two examples:
http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/
http://leastprivilege.com/2013/11/15/adding-refresh-tokens-to-a-web-api-v2-authorization-server/
As broadly described in the first link, you can create your own token provider implementation to handle token refresh:
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var clientid = context.Ticket.Properties.Dictionary["as:client_id"];
if (string.IsNullOrEmpty(clientid))
{
return;
}
var refreshTokenId = Guid.NewGuid().ToString("n");
using (AuthRepository _repo = new AuthRepository())
{
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
var token = new RefreshToken()
{
Id = Helper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;
token.ProtectedTicket = context.SerializeTicket();
var result = await _repo.AddRefreshToken(token);
if (result)
{
context.SetToken(refreshTokenId);
}
}
}
}

Resources