I am using IdentityModel.AspNetCore and .AddClientAccessTokenHandler() extension to automatically supply HttpClient with access token (at least that is what I understand I can use it for) to an API. Some API endpoints are authorized based on a role. But for some reason, the access token that is added to the request does not contain the role claim. If I do not use the .AddClientAccessTokenHandler() and manually retrieve the token and set it using SetBearerToken(accessTone) then I can reach my role authorized endpoint.
My startup is:
services.AddAccessTokenManagement(options =>
{
options.Client.Clients.Add("auth", new ClientCredentialsTokenRequest
{
Address = "https://localhost:44358/connect/token",
ClientId = "clientId",
ClientSecret = "clientSecret",
});
});
WebApi call:
var response = await _httpClient.GetAsync("api/WeatherForecast/SecretRole");
Identity server configuration:
public static IEnumerable<ApiResource> GetApis() =>
new List<ApiResource>
{
new ApiResource("WebApi", new string[] { "role" })
{ Scopes = { "WebApi.All" }}
};
public static IEnumerable<ApiScope> GetApiScopes() =>
new List<ApiScope>
{ new ApiScope("WebApi.All") };
public static IEnumerable<IdentityResource> GetIdentityResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource
{
Name = "roles",
UserClaims = { "role" }
}
};
public static IEnumerable<Client> GetClients() =>
new List<Client>
{
new Client
{
ClientId = "clientId",
ClientSecrets = { new Secret("clientSecret".ToSha256()) },
AllowedGrantTypes =
{
GrantType.AuthorizationCode,
GrantType.ClientCredentials
},
AllowedScopes =
{
"WebApi.All",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"roles"
},
RedirectUris = { "https://localhost:44305/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:44305/Home/Index" },
AlwaysIncludeUserClaimsInIdToken = false,
AllowOfflineAccess = true,
}
};
For testing purposes I add users manually from Program.cs
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
AddUsers(userManager).GetAwaiter().GetResult();
}
host.Run();
}
private static async Task AddUsers(UserManager<IdentityUser> userManager)
{
var adminClaim = new Claim("role", "Admin");
var visitorClaim = new Claim("role", "Visitor");
var user = new IdentityUser("Admin");
await userManager.CreateAsync(user, user.UserName);
await userManager.AddClaimAsync(user, adminClaim);
user = new IdentityUser("Visitor");
await userManager.CreateAsync(user, user.UserName);
await userManager.AddClaimAsync(user, visitorClaim);
}
So if I use manual access token retrieval and add it myself to the HttpClient headers, then my endpoint is reached and returns expected response. If I use .AddClientAccessTokenHandler(), I get 403 - Forbidden. What am I missing?
Since you are registering the client under the name auth, you also should retrieve it as such.
This basically means I expect you to use something like this, or it's equivalent:
_httpClient = factory.CreateClient("auth");
Basically this mechanism ensures you're able to retrieve HttpClients for various API's and settings.
ps. I am on mobile; and currently not very good access to my resources.
Related
I'm learning angular and I find a problem that I didn't see anywhere else. To the point, when I'm trying to login to my client angular app with oidc-client via identityserver, It's working great up to a point. I'm redirecting to a identityserver, logged in, goes back to the login-callback, in server-side i'm authorized, but if I want to check if I'm authorized in client-side, I got a null user. If i want to logout, I got redirect to sign out in identityserver where I can see my token:
Server-side config.cs:
public class Config
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Email(),
new IdentityResources.Profile(),
new IdentityResource("RedDot.API.read", new[] { JwtClaimTypes.Name, JwtClaimTypes.Email, "location" })
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("RedDot.API.read", "Resource API")
{
ApiSecrets =
{
new Secret("secret".Sha256())
},
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email
},
Scopes = new List<string> { "RedDot.API.read" }
}
};
}
public static IEnumerable<Client> GetClients()
{
return new[]
{
new Client {
RequireConsent = false,
ClientId = "reddot_ui",
ClientName = "RedDot",
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"RedDot.API.read"
},
RedirectUris = {"https://localhost:4200/login-callback"},
PostLogoutRedirectUris = {"https://localhost:4200/"},
AllowedCorsOrigins = {"https://localhost:4200"},
AllowAccessTokensViaBrowser = true,
AccessTokenLifetime = 3600,
ClientSecrets =
{
new Secret("rwebv832hvegfh49--l-w".Sha256())
},
}
};
}
}
Program.cs:
builder.Services.AddDbContext<AppIdentityDbContext>(options => options.UseSqlServer(dataConnectionString, conf => conf.MigrationsAssembly(assembly)));
builder.Services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddIdentityServer().AddDeveloperSigningCredential()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(dataConnectionString, conf => conf.MigrationsAssembly(assembly));
})
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<AppUser>();
Client-side angular config:
private getUserManager() {
if (!this._userManager) {
const userManagerSettings: UserManagerSettings =
new UserManagerSettings();
userManagerSettings.authority = 'https://localhost:5443';
userManagerSettings.client_id = 'reddot_ui';
userManagerSettings.response_type = 'code';
userManagerSettings.scope = 'openid profile RedDot.API.read';
userManagerSettings.redirect_uri = 'https://localhost:4200/login-callback';
userManagerSettings.post_logout_redirect_uri = 'https://localhost:4200/logout-callback';
userManagerSettings.automaticSilentRenew = true;
userManagerSettings.silent_redirect_uri = 'https://localhost:4200/silent-callback';
userManagerSettings.userStore = new WebStorageStateStore({
store: window.localStorage,
}); // store information about Authentication in localStorage
this._userManager = new UserManager(userManagerSettings);
this._userManager.getUser().then((user) => {
this._user = user;
this.isUserDefined = true;
});
}
}
Rest of authorize class in angular:
import { Injectable } from "#angular/core";
import { User, UserManager, WebStorageStateStore } from "oidc-client";
import { UserManagerSettings } from "../_models/usermanager.settings";
#Injectable()
export class AuthenticationService {
isUserDefined = false;
private _user: User | null;
private _userManager: UserManager;
isLoggedIn() {
return this._user != null && !this._user.expired;
}
getName() {
return this._user?.profile.nickname;
}
getAccessToken() {
return this._user ? this._user.access_token : "";
}
getClaims() {
return this._user?.profile;
}
startAuthentication() : Promise<void> {
this.getUserManager();
return this._userManager.signinRedirect();
}
completeAuthentication() {
this.getUserManager();
return this._userManager.signinRedirectCallback().then((user) => {
this._user = user;
this.isUserDefined = true;
});
}
startLogout(): Promise<void> {
this.getUserManager();
return this._userManager.signoutRedirect();
}
completeLogout() {
this.getUserManager();
this._user = null;
return this._userManager.signoutRedirectCallback();
}
silentSignInAuthentication() {
this.getUserManager();
return this._userManager.signinSilentCallback();
}
}
Dunno where the problem can be and why I'm not authorized on client-side.
I've tried to change response type and protocol from http to https and conversely with no effect. Maybe somebody had the same problem.
The code you provided doesn't look like there is anything wrong. According to your description, you can log in and log out successfully, but you are not authorized. Does it mean that you cannot access the corresponding resources? Do you have any error messages?
I used https://demo.duendesoftware.com site to test with Angular application (here is the code of Angular), but did not reproduce your situation. You can see how the configuration items in it perform, and whether your configuration is missing some steps compared to it.
In addition, I found a sample code that is similar to your Angular program, but the Scope configuration is slightly different from yours. I am not sure if this is the reason, but you can use it as a reference
I have the following code in my Startup.cs
var openIdOptions = new OpenIdConnectAuthenticationOptions
{
ClientId = openIdConfiguration.ClientId,
Authority = openIdConfiguration.Authority,
RedirectUri = openIdConfiguration.RedirectUri,
ResponseType = openIdConfiguration.CodeFlowEnabled ? "code" : OpenIdConnectResponseTypes.IdToken,
TokenValidationParameters = tokenValidationParameters,
Scope = string.IsNullOrEmpty(openIdConfiguration.Scope) ? OpenIdConnectScopes.OpenIdProfile : openIdConfiguration.Scope,
PostLogoutRedirectUri = openIdConfiguration.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = this.AuthorizationCodeReceived,
MessageReceived = this.MessageReceived,
SecurityTokenValidated = this.SecurityTokenValidated,
SecurityTokenReceived = this.SecurityTokenReceived,
AuthenticationFailed = this.AuthenticationFailed,
RedirectToIdentityProvider = this.RedirectToIdentityProvider
},
SignInAsAuthenticationType = "Cookies",
CookieManager = new SameSiteCookieManager(new SystemWebCookieManager())
};
....
app.UseOpenIdConnectAuthentication(openIdOptions);
I would like to generate a token from a separate TokenService.cs, with a different configuration which will be consumed by another application.
public class TokenService
{
public async Task<string> GenerateToken()
{
var openIdOptions = new OpenIdConnectAuthenticationOptions
{
//similar OIDC config
}
**// How to generate a token ?**
return token;
}
}
How to go about generating a new token when needed from my TokenService class ?
In the discovery document, the scope IdentityPortal.API is not added
{
"issuer": "https://localhost:5001",
"scopes_supported": ["profile", "openid", "email", "offline_access"],
}
However, allowed scope in the config is as below
private static string apiScope = "IdentityPortal.API";
private static ICollection<string> AllowedScopes()
{
return new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
apiScope
};
}
API Resource
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource(apiScope, "Falcon Api")
{
Scopes = new List<string>{apiScope},
UserClaims =
{
JwtClaimTypes.Profile,
JwtClaimTypes.Name,
JwtClaimTypes.Email,
}
}
};
}
From the React Application I am sending scope as below
scope: "profile openid email IdentityPortal.API offline_access",
In the identity server IdentityPortal.API is not added as supported claim.
Here is the customPersistedGrantStore.cs
public class CustomResourceStore : IResourceStore
{
protected IRepository _dbRepository;
public CustomResourceStore(IRepository repository)
{
_dbRepository = repository;
}
public Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeNameAsync(IEnumerable<string> scopeNames)
{
var list = _dbRepository.Where<IdentityResource>(e => scopeNames.Contains(e.Name));
return Task.FromResult(list.AsEnumerable());
}
public Task<IEnumerable<ApiScope>> FindApiScopesByNameAsync(IEnumerable<string> scopeNames)
{
var list = _dbRepository.Where<ApiScope>(a => scopeNames.Contains(a.Name));
return Task.FromResult(list.AsEnumerable());
}
public Task<IEnumerable<ApiResource>> FindApiResourcesByScopeNameAsync(IEnumerable<string> scopeNames)
{
var list = _dbRepository.Where<ApiResource>(a => a.Scopes.Any(s => scopeNames.Contains(s)));
return Task.FromResult(list.AsEnumerable());
}
public Task<IEnumerable<ApiResource>> FindApiResourcesByNameAsync(IEnumerable<string> apiResourceNames)
{
var list = _dbRepository.Where<ApiResource>(a => apiResourceNames.Contains(a.Name));
return Task.FromResult(list.AsEnumerable());
}
public Task<Resources> GetAllResourcesAsync()
{
var result = new Resources(GetAllIdentityResources(), GetAllApiResources(),null);
return Task.FromResult(result);
}
private IEnumerable<IdentityResource> GetAllIdentityResources()
{
return _dbRepository.All<IdentityResource>();
}
private IEnumerable<ApiResource> GetAllApiResources()
{
return _dbRepository.All<ApiResource>();
}
private IEnumerable<ApiScope> GetAllApiScopes()
{
return _dbRepository.All<ApiScope>();
}
}
Identity server setup
services.Configure<MongoDbConfigurationOptionsViewModel>(Configuration);
services.AddIdentityServer()//.AddProfileService<ProfileService>()
.AddMongoRepository()
.AddMongoDbForAspIdentity<ApplicationUser, IdentityRole>(Configuration)
.AddClients()
.AddInMemoryApiScopes(Config.AllowedScopes())
.AddIdentityApiResources()
.AddPersistedGrants()
.AddDeveloperSigningCredential();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
// base-address of your identityserver
options.Authority = "https://localhost:5001";
// name of the API resource
options.ApiName = "IdentityPortal.API";
});
Config
public static IEnumerable<ApiScope> AllowedScopes()
{
return new List<ApiScope>
{
new ApiScope(IdentityServerConstants.StandardScopes.OpenId),
new ApiScope(IdentityServerConstants.StandardScopes.Profile),
new ApiScope(IdentityServerConstants.StandardScopes.Email),
new ApiScope(apiScope)
};
}
Issue is that you just added the api resources on IDS4 setup, you need to change your code to add API scopes too. To add api scopes you have above you can add it via AddInMemoryApiScopes. Code would like this:
services.Configure<MongoDbConfigurationOptionsViewModel>(Configuration);
services.AddIdentityServer()//.AddProfileService<ProfileService>()
.AddMongoRepository()
.AddMongoDbForAspIdentity<ApplicationUser, IdentityRole>(Configuration)
.AddClients()
.AddInMemoryApiScopes(Config.AllowedScopes)
.AddIdentityApiResources()
.AddPersistedGrants()
.AddDeveloperSigningCredential();
Once made the code change, regenerate the token and check it on https://jwt.ms/ you should have a prop as aud = IdentityPortal.API and also scope as IdentityPortal.API
As you are using DB, you need to migrate your DB to new version first, here are scripts to help on this: https://github.com/RockSolidKnowledge/IdentityServer4.Migration.Scripts/tree/CreateScripts
After DB update make sure you have data on api resource and also api resource's scope matching the required scopes
Checkout my blog post https://github.com/nahidf-adventures/IdentityServer4-adventures/tree/ids4-4/src/IdentityServer for more detailed explanation.
Read more on official docs here
I have to connect my existing MVC project written in .net 4.5 to IdentityServer4. Yesterday I tried this solution: https://stackoverflow.com/a/39771372, and it's working, but I need to handle callback and load additional user data from client app database. I've tried to change RedirectUri to my own Controller method, but it is not called.
Today I trying to use Implicit Flow from sample: https://github.com/IdentityServer/IdentityServer4.Samples/tree/release/Clients/src/MvcManual - i know that sample is net.core, but now I can handle callback from IdentityServer. Now how can I validate token? I have problem with whole ValidateIdentityToken method
I decoded token using:
var token = new JwtSecurityToken(jwtEncodedString: idToken);
In token I found my user info, but I think it is not safe method.
Finally I have two questions:
Can I simply configure my client to redirect to my controller method where I loading additional user data like "sitemap" from Database? It is possible using first sample?
Using second sample, anyone knows how to validate token in net4.5 client?
After validation I need only Subject and username to load additional data.
EDIT: My configuration
IdentityServer4 client configuration:
a) Hybrid
new Client
{
ClientName = "mvc Client",
ClientId = "mvc",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RedirectUris = { "http://localhost:5002/Account/Callback" }, // where to redirect to after login
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, // where to redirect to after logout
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
},
ClientSecrets =
{
new Secret("secret".Sha256())
},
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true
}
b) Implicit method
new Client
{
ClientName = "mvc Client",
ClientId = "mvc",
AllowedGrantTypes = {GrantType.ClientCredentials, GrantType.Implicit},
RedirectUris = { "http://localhost:5002/Account/Callback" }, // where to redirect to after login
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, // where to redirect to after logout
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
},
ClientSecrets =
{
new Secret("secret".Sha256())
},
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true
}
MVC .net4.5 configuration
a) using Microsoft.Owin.Security.OpenIdConnect, where RedirectUri is not called
Startup.cs
public void ConfigureAuthentication(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "http://localhost:5000", //ID Server
ClientId = "mvc",
ResponseType = "id_token code",
SignInAsAuthenticationType = "Cookies",
RedirectUri = "http://localhost:5002/Account/Callback", //URL of website
Scope = "openid profile",
});
}
b) Using second method Implicit
Startup.cs
public void ConfigureAuthentication(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
LoginPath = new PathString("/Account/Secure")
});
}
AccountController
public class AccountController : BaseController
{
[AllowAnonymous]
public async Task<ActionResult> Secure()
{
return await StartAuthentication();
}
public async Task<ActionResult> Logout()
{
Authentication.SignOut("Cookies");
var disco = await DiscoveryClient.GetAsync(Constants.Authority);
return Redirect(disco.EndSessionEndpoint);
}
[AllowAnonymous]
private async Task<ActionResult> StartAuthentication()
{
// read discovery document to find authorize endpoint
var disco = await DiscoveryClient.GetAsync(Constants.Authority);
var authorizeUrl = new AuthorizeRequest(disco.AuthorizeEndpoint).CreateAuthorizeUrl(
clientId: "mvc",
responseType: "id_token",
scope: "openid profile",
redirectUri: "http://localhost:5002/Account/Callback",
state: "random_state",
nonce: "random_nonce",
responseMode: "form_post");
return Redirect(authorizeUrl);
}
public async Task<ActionResult> Callback()
{
var state = Request.Form["state"].ToString();
var idToken = Request.Form["id_token"].ToString();
if (!string.Equals(state, "random_state")) throw new Exception("invalid state");
var user = await ValidateIdentityToken(idToken, state);
var id = new ClaimsIdentity(user, "Cookies");
Authentication.SignIn(new AuthenticationProperties
{
IsPersistent = false
}, id);
//Here Loading other data for user from Database e.g SiteMap
// {...}
return Redirect("/");
}
private async Task<ClaimsPrincipal> ValidateIdentityToken(string idToken)
{
// working area
var token = new JwtSecurityToken(jwtEncodedString: idToken);
Console.WriteLine("email => " + token.Claims.First(c => c.Type == "email").Value);
//working area
//need to return user Claims from validated token. How to?
}
BaseController
[Authorize]
public abstract class BaseController : Controller
{
public IAuthenticationManager Authentication
{
get { return this.HttpContext.GetOwinContext().Authentication; }
}
// Methods that can be used in my other controllers.
}
I am having trouble getting IdentityServer to return any claims on a resource scope. I define the resource scope as:
private static Scope Roles
{
get
{
return new Scope
{
Name = "roles",
DisplayName = "Roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim(SecurityContants.ClaimTypes.Role)
}
};
}
}
private static Scope Api
{
get
{
return new Scope
{
Name = "api",
DisplayName = "Api",
IncludeAllClaimsForUser = true,
//i've also tried adding the claim directly
Type = ScopeType.Resource,
Emphasize = false
};
}
}
Then they're added to a collection and handed back.
In my API Startup.cs, I configure owin like:
public void Configuration (IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost/security.tokenserver.site",
RequiredScopes = new[] { "api", "roles" },
RoleClaimType = "roles"
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
My API endpoint is simply:
public IEnumerable<string> Get()
{
var user = User as ClaimsPrincipal;
var claims = new List<string>();
foreach (var item in user.Claims)
{
claims.Add(item.Value);
}
return claims;
}
The output of this, when called from my UI after authenticating is:
[ "customer_svc", "api", "https://idsrv3/embedded", "https://idsrv3/embedded/resources", "1448048492", "1448044892" ]
So the initial login at the UI works (i see the roles claim there), retrieving the token works, and handing the token up to the API works. But I cannot see the user's roles after the token is handed back up to the API.
What am I missing?