I am new to Identity Server. I haven't configured it before. But I need it for a Project I am working on.
The API will be serving an Angular JS Client, iOS App and an Android App. We need to implement authentication and authorisation and custmer grant
Note: I am trying to configure Identity Server and my API in the same Web API project.
I have followed the documentation and configured Identity Server as the following:
In startup.cs, in ConfigureServices()
private readonly IConfiguration config;
private const string DEFAULT_CORS_POLICY = "localhost";
public Startup (IConfiguration config) => this.config = config;
public void ConfigureServices (IServiceCollection services) {
services.AddIdentityServer ()
.AddDeveloperSigningCredential ()
//.AddInMemoryApiResources(config.GetSection("ApiResources"))
.AddInMemoryApiResources (Config.GetApis ())
//.AddInMemoryClients(config.GetSection("Clients"))
.AddInMemoryClients (Config.GetClients ())
.AddInMemoryIdentityResources (Config.GetIdentityResources ())
//.AddInMemoryIdentityResources(config.GetSection("IdentityResources"))
.AddExtensionGrantValidator<WechatGrantValidator> ();
services.AddTransient<IUserCodeValidator, UserCodeValidator> ();
services.AddCors (options => {
options.AddPolicy (DEFAULT_CORS_POLICY, builder => {
builder.WithOrigins ("http://localhost:5202");
builder.AllowAnyHeader ();
builder.AllowAnyMethod ();
});
});
}
I implemented the interface IExtensionGrantValidator and register the extension grant
public class WechatGrantValidator : IExtensionGrantValidator {
private IUserCodeValidator validator;
public WechatGrantValidator (IUserCodeValidator validator) {
this.validator = validator;
}
public string GrantType => "wechat_grant";
public async Task ValidateAsync (ExtensionGrantValidationContext context) {
string userCode = context.Request.Raw.Get ("userCode");
var result = await validator.ValidateAsync (userCode);
if (result.IsError) {
context.Result = new GrantValidationResult (TokenRequestErrors.InvalidGrant);
return;
}
context.Result = new GrantValidationResult (result.UserId, GrantType);
return;
}
}
I have followed the documentation and configured client infos as the following
public static IEnumerable<Client> GetClients () {
return new Client[] {
new Client {
ClientId = "javascritpClient",
ClientName = "JavaScript Client",
AllowedGrantTypes = { "wechat_grant" },
AllowAccessTokensViaBrowser = true,
AllowedCorsOrigins = { "http://localhost:5202" },
AllowedScopes = { "api1" },
ClientSecrets = { new Secret ("secret".Sha256 ()) }
}
};
}
Now because I want to use it Angular JS, iOS and Android I want to just get the Access Token from the IdentityServer, and then use the Access Token for Authentication and Authorisation.
for this I am trying to access the /connect/token from a JS client
But I am getting an invalid_client error.
#Injectable()
export class OauthService {
private http: Http;
public constructor(http: Http) {
this.http = http;
}
public async getDiscoveryInfos(issuer: string): Promise<DiscoveryInfos> {
if (!issuer.endsWith('/')) {
issuer += '/';
}
issuer += '.well-known/openid-configuration';
return this.http.get(issuer).map(response => {
return response.json();
}).toPromise();
}
public async getToken(): Promise<any> {
const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
const discovery = await this.getDiscoveryInfos('http://localhost:5200');
return this.http.post(discovery.token_endpoint, {
grant_type: 'wechat_grant',
userCode: 'userCodeAA',
client_id: 'javascritpClient',
client_secret: 'secret',
scope:'api1'
}, { headers: headers }).map(response => response.json()).toPromise();
}
}
http response infos
The server response "error":"invalid_client"
log infos
The error I get on the server side is 'No client identifier found':
1 - Why am I getting this error?
2 - As I need to get the Token programmatically in JS, I need to use /connect/token, am I correct on this? Am I on the correct path?
in ng2 use a method like bellow:
public Token(data: SigninModel): Observable<any> {
this.options = new RequestOptions({ headers: this.headers });
this.headers.append('Content-Type', 'application/x-www-form-urlencoded');
const url = this.urlBase + `connect/token`;
const param = new URLSearchParams();
param.set('grant_type', 'password');
param.set('client_Id', 'javascritpClient');
param.set('client_secret', 'secret');
param.set('scope', 'offline_access');
param.set('username', data.username);
param.set('password', data.password);
return this.http.post(url, `${param.toString()}`, this.options)
.map((response: Response) => {
return (response.json());
})
.catch(this.handleError);
}
Related
Technology Stack
Using .NET CORE React Template
1 IIS Website
Application Pool (v4 Integrated)
Port 80
Clicking on a Register Button, calls the Register Component.
Component within a useEffect(), calls "/login URL using Axios
C# Map("/login") is called a Challenge to Authenticate using LinkedIn
The CORS Error is then returned
Error Snapshot 1 of 5
Snapshot 2 of 5; Snapshot 3 of 5; Snapshot 4 of 5; Snapshot 5 of 5
React Code
const url = `/login`;
const headers = {
'Content-Type': 'text/html'
}
axios({
method: 'get',
url: url,
headers: headers
})
.then((response) => {...})
.catch((error: Error | AxiosError) => {...});
C# Code - Linked Authentication, Cookies, CORS Middleware
Start.cs - ConfigureServices()
public void ConfigureServices(IServiceCollection services)
{
#region AddAuthentication, AddLinkedIn, AddCookie
services.AddAuthentication()
.AddLinkedIn(o =>
{
IConfigurationSection linkedinAuthNSection =
Configuration.GetSection("Authentication:Linkedin");
o.ClientId = linkedinAuthNSection["ClientId"];
o.ClientSecret = linkedinAuthNSection["ClientSecret"];
})
.AddCookie(o =>
{
o.LoginPath = "/login";
o.LogoutPath = "/logout";
});
#endregion
#region Global CORS Policy Declaration
services.AddCors(o =>
{
o.AddDefaultPolicy(builder =>
builder.AllowAnyMethod()
.AllowAnyHeader()
.AllowAnyOrigin()
);
});
#endregion
services.AddControllersWithViews();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "client-app/build";
});
}
Start.cs - Configure()
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
#region Map/login
app.Map("/login", builder =>
{
builder.Run(async context =>
{
var properties = new AuthenticationProperties() { RedirectUri = "/" };
await context.ChallengeAsync("LinkedIn", properties);
});
});
#endregion
#region Map/logout
app.Map("/logout", builder =>
{
builder.Run(async context =>
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
context.Response.Redirect("/");
});
});
#endregion
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = Path.Join(env.ContentRootPath, "client-app");
if (env.IsDevelopment())
{
spa.Options.StartupTimeout = TimeSpan.FromSeconds(240);
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
Finally, I got it working!
I have made 2 major changes, after trying to fathom, understand internalizing. Thanks to the yellow warning symbols ⚠ of Chrome Dev Tools, that let me to this article and change 1 of the solution.
Change 1
Applied the major snippets of the above example code to my React SPA .NET Core project
Dropped the Map Middleware (app.Map("/login") that allows branching of the pipeline path.
In favor for a .NET Controller/Action.
However more specifically, just Action since the "/login" is added to the path, of the URL, which makes it difficult to accept the successful sign-in.
Change 2
Dropped Axios call only for the Authentication UI Interaction, since LinkedIn, does not support it. LinkedIn OAuth redirect login returning "No 'Access-Control-Allow-Origin' header is present on the requested resource" error
In favour of using HTML href.
Authentication.cs
//[Route("[controller]/[action]")]
[Route("[action]")]
public class AuthenticationController : Controller
{
[HttpGet]
public IActionResult Register(string authType = "LinkedIn")
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/" });
}
[HttpGet]
public IActionResult Login(string authType = "LinkedIn")
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/" });
}
[HttpGet]
public IActionResult Logout()
{
return SignOut();
}
Start.cs ConfigureServices()
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = "LinkedIn";
})
.AddCookie()
.AddOAuth("LinkedIn", o =>
{
o.CorrelationCookie.HttpOnly = true;
o.CorrelationCookie.SameSite = SameSiteMode.Lax;
var linkedInSection = Configuration.GetSection("Authentication:LinkedIn");
o.ClientId = linkedInSection.GetSection("ClientId").Get<string>();
o.ClientSecret = linkedInSection.GetSection("ClientSecret").Get<string>();
o.CallbackPath = new PathString(linkedInSection.GetSection("CallbackPath").Get<string>());
o.AuthorizationEndpoint = linkedInSection.GetSection("AuthorizationEndpoint").Get<string>();
o.TokenEndpoint = linkedInSection.GetSection("TokenEndpoint").Get<string>();
o.UserInformationEndpoint = linkedInSection.GetSection("UserInformationEndpoint").Get<string>();
o.Scope.Add("r_liteprofile");
o.Scope.Add("r_liteprofile");
o.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(json.RootElement);
}
};
});
}
Start.cs Configure()
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
}
In your Chrome browser, try installing an extension called Access control Allow Origin from the Chrome web store . And Open the Options or settings page of that extension and type your localhost address in the textbox like this : https://localhost:80 in ur case
I had faced this issue once and this extension worked for me..
We have users complaining because they are redirected to the login page of the Identity Server while in the middle of their work (and thus losing their current work). We have endeavoured to configure a sliding expiration, so I'm not sure why this is happening.
I realise there is quite a bit of code in this post. But there are a lot of moving parts and I want to give as much information as possible.
This behavour is arratic and it is hard to report an exact reproducable event. In my testing, I've been ejected at random times and it is hard to understand whether it has any relationship to any of the cofigurations which I have set. In my mind, I should not be ejected at all, as a silent sign-in is always sent during the addAccessTokenExpiring event.
The setup that we have is:
an Idp (using IdentityServer 4)
A client app, implemented using Vue.js (using Typescript)
An API, written in ASP.NET Core 5
The config and auth service which we have written are:
auth.config.ts
import { Log, UserManagerSettings, WebStorageStateStore } from "oidc-client";
import AppConfig from "./invariable/app.config";
/* eslint-disable */
class AuthConfig {
public settings: UserManagerSettings;
private baseUrl: string;
constructor() {
this.baseUrl = AppConfig.RunTimeConfig.VUE_APP_APPURL || process.env.VUE_APP_APPURL;
this.settings = {
userStore: new WebStorageStateStore({ store: window.localStorage }),
authority: AppConfig.RunTimeConfig.VUE_APP_IDPURL || process.env.VUE_APP_IDPURL,
client_id: AppConfig.RunTimeConfig.VUE_APP_CLIENTID || process.env.VUE_APP_CLIENTID,
client_secret: AppConfig.RunTimeConfig.VUE_APP_CLIENTSECRET || process.env.VUE_APP_CLIENTSECRET,
redirect_uri: this.baseUrl + process.env.VUE_APP_AUTHCALLBACK,
automaticSilentRenew: false,
silent_redirect_uri: this.baseUrl + process.env.VUE_APP_SILENTREFRESH,
response_type: "code",
response_mode: "query",
scope: "our_scopes",
post_logout_redirect_uri: this.baseUrl + process.env.VUE_APP_SIGNOUT_CALLBACK,
filterProtocolClaims: true,
loadUserInfo: true,
revokeAccessTokenOnSignout: true,
staleStateAge: 300, // should match access_token lifetime.
};
}
}
/* eslint-enable */
const authConfig = new AuthConfig();
export default authConfig;
auth.service.ts
import { UserManagerSettings, User, UserManager } from "oidc-client";
import authConfig from "#/config/auth.config";
import axios, { AxiosResponse } from "axios";
import { Ajax } from "#/config/invariable/ajax";
import AccessClaim from "#/domain/general/accessclaim";
import _ from "lodash";
import store from "#/store";
import StoreNamespaces from "#/config/invariable/store.namespaces";
import Token from "#/store/token/token";
export class AuthService {
private userManager: UserManager;
private tokenStore: string;
constructor(private settings: UserManagerSettings) {
this.settings = settings;
this.userManager = new UserManager(this.settings);
this.tokenStore = StoreNamespaces.tokenModule;
}
public addEvents(): void {
this.userManager.events.addUserSignedOut(() => {
this.signInAgain();
});
this.userManager.events.addAccessTokenExpired(() => {
console.log("Token expired");
this.clearLocalState();
console.log("Stale state cleaned up");
});
this.userManager.events.addAccessTokenExpiring(() => {
console.log("Access token about to expire.");
this.signInAgain();
});
this.userManager.events.addSilentRenewError(() => {
// custom logic here
console.log("An error happened whilst silently renewing the token.");
});
}
public clearLocalState(): Promise<void> {
return this.userManager.clearStaleState();
}
public getUserOnLoad(): Promise<User> {
return this.userManager.getUser().then((user) => {
if (!_.isNil(user) && !user.expired) {
console.log("first load sign-in");
const decodedIdToken = user.profile;
if (!_.isNil(decodedIdToken.store) && !_.isArray(decodedIdToken.store)) decodedIdToken.store = [decodedIdToken.store];
if (!_.isNil(decodedIdToken.classification) && !_.isArray(decodedIdToken.classification)) decodedIdToken.classification = [decodedIdToken.classification];
if (!_.isNil(decodedIdToken.location) && !_.isArray(decodedIdToken.location)) decodedIdToken.location = [decodedIdToken.location];
if (!_.isArray(decodedIdToken.app)) decodedIdToken.app = [decodedIdToken.app];
const token = new Token();
token.accessToken = user.access_token;
token.idToken = user.id_token;
token.storeClaims = decodedIdToken.store || [];
token.userType = decodedIdToken.usertype;
token.isLoggedIn = user && !user.expired;
token.app = decodedIdToken.app;
token.userName = decodedIdToken.name ?? "Unknown User";
store.dispatch(`${this.tokenStore}/setToken`, token);
return user;
} else {
return this.signInAgain();
}
});
}
public async getUserIfLoggedIn(): Promise<User | null> {
const currentUser: User | null = await this.userManager.getUser();
const loggedIn = currentUser !== null && !currentUser.expired;
return loggedIn ? currentUser : null;
}
public async isLoggedIn(): Promise<boolean> {
const currentUser: User | null = await this.userManager.getUser();
return currentUser !== null && !currentUser.expired;
}
public login(): Promise<void> {
return this.userManager.signinRedirect();
}
public logout(): Promise<void> {
return this.userManager.signoutRedirect();
}
public getAccessToken(): Promise<string> {
return this.userManager.getUser().then((data: any) => {
return data.access_token;
});
}
public signInAgain(): Promise<User> {
return this.userManager
.signinSilent()
.then((user) => {
console.log("silent sign-in");
const decodedIdToken = user.profile;
if (!_.isNil(decodedIdToken.store) && !_.isArray(decodedIdToken.store)) decodedIdToken.store = [decodedIdToken.store];
if (!_.isNil(decodedIdToken.classification) && !_.isArray(decodedIdToken.classification)) decodedIdToken.classification = [decodedIdToken.classification];
if (!_.isNil(decodedIdToken.location) && !_.isArray(decodedIdToken.location)) decodedIdToken.location = [decodedIdToken.location];
if (!_.isArray(decodedIdToken.app)) decodedIdToken.app = [decodedIdToken.app];
const token = new Token();
token.accessToken = user.access_token;
token.idToken = user.id_token;
token.storeClaims = decodedIdToken.store || [];
token.userType = decodedIdToken.usertype;
token.isLoggedIn = user && !user.expired;
token.app = decodedIdToken.app;
token.userName = decodedIdToken.name ?? "Unknown User";
store.dispatch(`${this.tokenStore}/setToken`, token);
return user;
})
.catch((err) => {
console.log("silent error");
console.log(err);
this.login();
return err;
});
}
public getAccessClaims(userDetails: any): Promise<AxiosResponse<any>> {
return axios.post(`${Ajax.appApiBase}/PermittedUse/GetAccessesForUser`, userDetails).then((resp: AxiosResponse<any>) => {
return resp.data;
});
}
public getPermissions(userDetails: any, siteId: number | null): Promise<AxiosResponse<any>> {
return axios.get(`${Ajax.appApiBase}/PermittedUse/GetPermissions/${siteId ?? 0}`).then((resp: AxiosResponse<any>) => {
return resp.data;
});
}
public constructAccess(userType: string, claims: Array<AccessClaim>): Array<AccessClaim> {
switch (userType) {
case "storeadmin":
case "storeuser":
return _.filter(claims, (claim) => {
return claim.claim === "store";
});
case "warduser":
return _.filter(claims, (claim) => {
return claim.claim === "classification";
});
}
return Array<AccessClaim>();
}
public getBookableLocations(userType: string, claims: Array<AccessClaim>): Array<AccessClaim> {
switch (userType) {
case "storeadmin":
case "storeuser":
return _.filter(claims, (claim) => {
return claim.claim === "store";
});
case "warduser":
return _.filter(claims, (claim) => {
return claim.claim === "location";
});
}
return Array<AccessClaim>();
}
}
export const authService = new AuthService(authConfig.settings);
On the Idp, our client configuration is:
ClientName = IcClients.Names.ConsumablesApp,
ClientId = IcClients.ConsumablesApp,
RequireConsent = false,
AccessTokenLifetime = TokenConfig.AccessTokenLifetime, // 300 for test purposes
IdentityTokenLifetime = TokenConfig.IdentityTokenLifetime, // 300
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Sliding,
UpdateAccessTokenClaimsOnRefresh = true,
RequireClientSecret = true,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
AllowAccessTokensViaBrowser = true,
AlwaysIncludeUserClaimsInIdToken = true,
RedirectUris = new List<string>
{
"https://localhost:44336/authcallback.html",
"https://localhost:8090/authcallback.html",
"https://localhost:44336/silent-refresh.html",
"https://localhost:8090/silent-refresh.html"
},
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44336/signout-callback-oidc.html",
"https://localhost:8090/signout-callback-oidc.html"
},
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.LocalApi.ScopeName,
IcAccessScopes.IcAccessClaimsScope,
IdentityResources.UserDetails,
IcAccessScopes.ConsumablesScope
},
ClientSecrets = { new Secret("oursecret".Sha256())}
At the Idp, we are using ASP.NET Core Identity:
services.AddIdentity<IdpUser, IdentityRole<int>>()
.AddEntityFrameworkStores<IdpDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options =>
{
options.ExpireTimeSpan = cookieDuration; // set to 1 hour
options.SlidingExpiration = true;
});
My expectation is that the sliding window should be extended every 5 minutes, as the user should be signed in again silently before the token expires.
When monitoring the IDP in my dev environment, one thing which I did note is that the checksession call is only being made once, when the user logs in. The wiki says that the checksession call should happen every 2s (by default). I have not changed this default (not knowingly). I even expressly set the checkSessionInterval property to 2000 to ensure that it was set to 2s.
The other thing I want to set out is the silent refresh html file, as I realise the CSP stuff can play into this:
<head>
<title></title>
<meta http-equiv="Content-Security-Policy" content="frame-src 'self' <%= VUE_APP_IDPURL %>; script-src 'self' 'unsafe-inline' 'unsafe-eval';" />
</head>
<body>
<script src="./oidc-client.min.js"></script>
<script>
(function refresh() {
window.location.hash = decodeURIComponent(window.location.hash);
new Oidc.UserManager({
// eslint-disable-next-line #typescript-eslint/camelcase
response_mode: "query",
userStore: new Oidc.WebStorageStateStore({
store: window.localStorage,
}),
})
.signinSilentCallback()
.then(function() {
console.log("****************************************signinSilentCallback****************************************");
})
.catch(function(err) {
debug;
console.log(err);
});
})();
</script>
</body>
If anyone can shed any light on this, it would be much appreciated.
Some further information. As a test, I set the refrsh time for the token and the cookie lifetime for the identity cookie to both be 10 hours (36,000s).
I am still getting reports of Users being kicked out after 45 minutes.
There's 2 conclusions I came to in resolving this and I would hesitate to say that it is a proper resolution to our problem.
There was a mistake in my Login code. I was using the IdentityServer4 extensions HttpContext.SignInAsync(user, authProperties) to create the session cookie. This is not the way to do things, if using ASP.NET Identity. For 1 reason, it does not include the SecurityStamp claim in the cookie. In their own quickstart, they use the SignInManager to sign in and issue the cookie _signInManager.PasswordSignInAsync(idpUser, model.Password, model.RememberLogin, true).
I needed to turn off the SecurityStamp validation feature in ASP.NET Identity. We had adjudged that we can live without it. You would think there would be a config setting for this, but I was not able to find it. So, at this stage, I have subclassed the UserManager and overridden the SupportsUserSecurityStamp property as follows: public override bool SupportsUserSecurityStamp => false;. Theoretically, this feature will now be turned off. Happy to be corrected if that is not the case or if there is a better way of doing it. (Would love to hear from a member of the ASP.NET team on this).
That's it.
I have the following flow Asp.Net Core App (App) that calls Asp.Net Core Web API (API1) that inturn calls another Asp.Net Web API (API2)
I'm using IdentityServer4 with Windows Authentication to Authenticate my users
here is my code:
ApiResources definition:
new ApiResource("api1", "api1", new List<string> { JwtClaimTypes.Name, JwtClaimTypes.Email}),
new ApiResource("api2", "api2", new List<string> { JwtClaimTypes.Name, JwtClaimTypes.Email})
Clients definition
new Client
{
ClientId = "app1",
ClientName = "app1",
ClientSecrets = { new Secret("app1".Sha256())},
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
IdentityServerConstants.StandardScopes.Email,
"api1"
},
RedirectUris = { "https://localhost:44375/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:44375/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:44375/signout-callback-oidc" },
AllowOfflineAccess = true,
RequireConsent = false,
AccessTokenLifetime = 5
},
new Client
{
ClientId = "api1",
ClientSecrets = { new Secret("api1".Sha256())},
AllowedGrantTypes = {"delegation" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api2"}
}
Delegation code in API1
public async Task<string> DelegateAsync(string userToken)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:44382/");
if (disco.IsError) throw new Exception(disco.Error);
var tokenResponse = await client.RequestTokenAsync(new TokenRequest()
{
Address = disco.TokenEndpoint,
GrantType = "delegation",
ClientId = "api1",
ClientSecret = "api1",
Parameters =
{
{"scope" , "api2 email profile openid" },
{"token", userToken }
}
});
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
_logger.LogInformation($"new: {tokenResponse.AccessToken}");
return tokenResponse.AccessToken;
}
IdentityServer4 DelegationGrantValidator:
public class DelegationGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _validator;
public DelegationGrantValidator(ITokenValidator validator)
{
_validator = validator;
}
public string GrantType => "delegation";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("token");
if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken);
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// get user's identity
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType);
return;
}
}
in API1 User.Identity.Name = "Domain\UserName"
but in API2 User.Identity.Name = null
is there anything missing that I should do solve this issue?
P.S.: if I call the IdentityServer UserInfo endpoint from API2 I'll get the expected UserName
After a lot of search I have finally found the answer.
I've followed what Rory Braybrook described in this Article and everything is working now
There are four clients within the application:
angular.application - resource owner
identity_ms.client - webapi app (.net core 2.1)
IdentityServer4 with AspNetIdentity
AccountController with shared actions to register users, reset password etc.
UserController with secured actions.
The Data action of the UserController has an [Authorize(Policy = "user.data")] attribute
ms_1.client - webapi app (.net core 2.1)
request.client - added specially to send requests from ms_1.client to identity_ms.client's UserController to get some user data.
I'm requesting clients using Postman:
http://localhost:identity_ms_port/connect/token to get access_token
http://localhost:ms_1_port/api/secured/action to get some secured data from ms_1
http://localhost:identity_ms_port/api/user/data to get some secured user data from identity_ms
Everything is working fine.
Also, ms_1 service has a secured action requesting http://localhost:identity_ms_port/api/user/data using System.Net.Http.HttpClient.
// identity_ms configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(/*cors options*/);
services
.AddMvc()
.AddApplicationPart(/*Assembly*/)
.AddJsonOptions(/*SerializerSettings*/)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
var clients = new List<Client>
{
new Client
{
ClientId = "angular.application",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "user.data.scope", "ms_1.scope", "identity_ms.scope" },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword
},
new Client
{
ClientId = "ms_1.client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "user.data.scope", "ms_1.scope" },
AllowedGrantTypes = GrantTypes.ClientCredentials
},
new Client
{
ClientId = "identity_ms.client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes =
{
"user.data.scope",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
AllowedGrantTypes = GrantTypes.Implicit
},
new Client
{
ClientId = "request.client",
AllowedScopes = { "user.data.scope" },
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
}
}
};
var apiResources = new List<ApiResource>
{
new ApiResource("ms_1.scope", "MS1 microservice scope"),
new ApiResource("identity_ms.scope", "Identity microservice scope"),
new ApiResource("user.data.scope", "Requests between microservices scope")
};
var identityResources = new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
services
.AddAuthorization(options => options.AddPolicy("user.data", policy => policy.RequireScope("user.data.scope")))
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(identityResources)
.AddInMemoryApiResources(apiResources)
.AddInMemoryClients(clients);
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "identity_ms.scope";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:identity_ms_port";
});
services.AddSwaggerGen(/*swagger options*/);
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<CustomMiddleware>();
app.UseIdentityServer();
app.UseAuthentication();
app.UseCors("Policy");
app.UseHttpsRedirection();
app.UseMvc(/*routes*/);
app.UseSwagger();
}
// ms_1.client configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(/*cors options*/);
services
.AddMvc()
.AddJsonOptions(/*SerializerSettings*/)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = "ms_1.scope";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:identity_ms_port";
});
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<CustomMiddleware>();
app.UseAuthentication();
app.UseCors("Policy");
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc(/*routes*/);
app.UseSwagger();
}
// ms_1.client action using HttpClient
[HttpPost]
public async Task<IActionResult> Post(ViewModel model)
{
//...
using (var client = new TokenClient("http://localhost:identity_ms_port/connect/token", "ms_1.client", "secret"))
{
var response = await client.RequestClientCredentialsAsync("user.data.scope");
if (response.IsError)
{
throw new Exception($"{response.Error}{(string.IsNullOrEmpty(response.ErrorDescription) ? string.Empty : $": {response.ErrorDescription}")}", response.Exception);
}
if (string.IsNullOrWhiteSpace(response.AccessToken))
{
throw new Exception("Access token is empty");
}
var udClient = new HttpClient();
udClient.SetBearerToken(response.AccessToken);
udClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var result = await udClient.GetAsync("http://localhost:identity_ms_port/api/user/data");
}
//...
}
I've tried the following:
To retrieve access_token from the request to ms_1 Authorization header and use it to access user/data.
To get new access_token to access user/data with it.
See public async Task<IActionResult> Post(ViewModel model) code within the code block.
In both cases, I've got the correct token which I can use to request both secured/action and user/data actions from Postman, but HttpClient is getting Unauthorized response (401).
Response headers screenshot
What am I doing wrong?
In your client code with HttpClient you are not requesting any scopes for the API therefore the token that is issued by Identity Server 4 will not contain the API as one of the audiences and then subsequently you will get 401 from API.
Change your token request to ask for the API scope as well.
var response = await client.RequestClientCredentialsAsync("user.data.scope ms_1.scope");
I want to implement JWT Authentication in react js using web api.
I had created the JWT Authentication in web api.
It worked totally fine on Postman as I tested it.
When I am using it with react js the API is being hitted.
Now the problem is how do I send the token to react js and how do I fetch the token in react js
This is my Login Controller in web api
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Web;
using System.Web.Http;
using System.Web.Http.Cors;
using WEBAPI_JWT_Authentication.Models;
namespace WEBAPI_JWT_Authentication.Controllers
{
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class LoginController : ApiController
{
[HttpPost]
public IHttpActionResult Authenticate([FromBody] LoginRequest login)
{
var loginResponse = new LoginResponse { };
LoginRequest loginrequest = new LoginRequest { };
loginrequest.Username = login.Username.ToLower();
loginrequest.Password = login.Password;
IHttpActionResult response;
HttpResponseMessage responseMsg = new HttpResponseMessage();
bool isUsernamePasswordValid = false;
if(login != null)
isUsernamePasswordValid=loginrequest.Password=="test" ? true:false;
// if credentials are valid
if (isUsernamePasswordValid)
{
string token = createToken(loginrequest.Username);
var responseJSON = token;
//return the token
return Ok(responseJSON);
}
else
{
// if credentials are not valid send unauthorized status code in response
loginResponse.responseMsg.StatusCode = HttpStatusCode.Unauthorized;
response = ResponseMessage(loginResponse.responseMsg);
return response;
}
}
private string createToken(string username)
{
//Set issued at date
DateTime issuedAt = DateTime.UtcNow;
//set the time when it expires
DateTime expires = DateTime.UtcNow.AddDays(7);
//http://stackoverflow.com/questions/18223868/how-to-encrypt-jwt-security-token
var tokenHandler = new JwtSecurityTokenHandler();
//create a identity and add claims to the user which we want to log in
ClaimsIdentity claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
});
const string sec = "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429090fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1";
var now = DateTime.UtcNow;
var securityKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.Default.GetBytes(sec));
var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(securityKey,Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature);
//create the jwt
var token =
(JwtSecurityToken)
tokenHandler.CreateJwtSecurityToken(issuer:"http://localhost:50191",audience:"http://localhost:50191",
subject: claimsIdentity, notBefore: issuedAt, expires: expires, signingCredentials: signingCredentials);
var tokenString = tokenHandler.WriteToken(token);
return tokenString;
}
}
}
This is where I am fetching the token in react js
function login(username, password) {
return fetch(`${API_URL}/Login`, {username, passowrd})
.then(response => {
debugger;
if (!response.ok) {
return response;
}
return response.json();
})
.then(user => {
debugger;
// login successful if there's a jwt token in the response
if (user && user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
}
return user;
});
}
Rather than this data if anyone knows how to send token to react js and how to fetch that token in react js, please do tell.
The way I do is to create a Response class
public class Response
{
public string Status { get; set; }
public string Message { get; set; }
public object Token { get; set; }
}
Depending on your need what you want to define in this class, for my cases, Status and Message is used to update progress status to front end.
You store your tokendictionary in class Response, and return it to the function. Lets say:
[HttpPost]
public IHttpActionResult Authenticate([FromBody] LoginRequest login)
{
var loginResponse = new LoginResponse { };
LoginRequest loginrequest = new LoginRequest { };
loginrequest.Username = login.Username.ToLower();
loginrequest.Password = login.Password;
IHttpActionResult response;
HttpResponseMessage responseMsg = new HttpResponseMessage();
bool isUsernamePasswordValid = false;
if(login != null)
isUsernamePasswordValid=loginrequest.Password=="test" ? true:false;
// if credentials are valid
if (isUsernamePasswordValid)
{
string token = createToken(loginrequest.Username);
Response Resp = new Response
{
Status = "Success",
Message = "User Login Successfully Change the Status Message here",
Token = tokenDictonary, //where you return token
};
return
}
else
{
// if credentials are not valid send unauthorized status code in response
loginResponse.responseMsg.StatusCode = HttpStatusCode.Unauthorized;
response = ResponseMessage(loginResponse.responseMsg);
return response;
}
}
and in your front end, you fetch your api.
I show you axios example
axios.post('http://yoururl', {
Token: this.state.Token,
})
.then(result => {
if (result.data.Status === 'Success') {
localStorage.setItem('Nameyourvariablehere', result.data.Token.tokentype);
// generate more if your token has more field
and then you are able to check your localStorage via getItem and setItem, I believe you know what to do for the following steps
Actually the way you create token is different from mine, I kind of follow this example.