Identity4Server many SPA clients best approach? - c#

Hi I have inherit a system like this:
An Api and many Fronts (spas) they share a common menu with links to navigate to each others but they are different react apps, with different urls. And Azure Active directory to authenticate an the Api is protected with Bearer token.
Something like this:
Now I have authorization requirements with a custom permissions that the business people want to assign to every user, for actions that they can do or not and visibility things.
I want to use Identity4Server with the active directory as an open id provider. Then consume a provider api to get custom permission and put those permissions in the claims. Then in the Api impl policies that demand for specify roles and claims to accomplish the permissions specifications.
Something like this:
Identity4Server config:
services.AddAuthentication()
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SaveTokens = true;
options.RequireHttpsMetadata = false;
options.Authority = "https://login.microsoftonline.com/tenant/";
options.ClientId = "ClientId";
options.ClientSecret = "ClientSecret";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
Api:
services
.AddAuthentication(configure =>
{
configure.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
configure.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "api";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
});
var clientsPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes("Bearer")
.AddRequirements(new ClaimsAuthorizationRequirement("ClientsModule", new[] { "1" }))
.RequireRole("Admin")
.Build();
services.AddAuthorization(options =>
{
options.AddPolicy("Clients", clientsPolicy);
});
For the react apps I'm using this npm "oidc-client": "1.7.0" and a similar approach to https://medium.com/#franciscopa91/how-to-implement-oidc-authentication-with-react-context-api-and-react-router-205e13f2d49
And the Clients config is: (Provider its quite similar the only thing that change is url localhost:3001)
export const IDENTITY_CONFIG = {
authority: "http://localhost:5000",
clientId: "fronts",
redirect_uri: "http://localhost:3000/signin-oidc",
login: "http://localhost:5000/login",
automaticSilentRenew: false,
loadUserInfo: false,
silent_redirect_uri: "http://localhost:3000/silentrenew",
post_logout_redirect_uri: "http://localhost:3000/signout-callback-oidc",
audience: "fronts",
responseType: "id_token token",
grantType: "password",
scope: "openid api",
webAuthResponseType: "id_token token"
};
If the user login into clients (localhost:3000) front and then navigate to providers (localhost:3001) front it shouldn't login again. To accomplish this I configure all the fronts with the same client id, but I don't know if this is the correct way to do it. Now my config class in identity server is:
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "fronts",
ClientSecrets =
{
new Secret("secret".Sha256())
},
ClientName = "All fronts",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:3000/signin-oidc", "http://localhost:3001/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:3000/signout-callback-oidc", "http://localhost:3001/signout-callback-oidc" },
AllowedCorsOrigins = { "http://localhost:3000", "http://localhost:3001" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api"
}
}
};
}
Do you think this configuration is the correct way to do it or there is a better approach?

You mentioned
many different react apps, with different urls
but in your code snippet I see only the Clients(localhost:3000).
Anyway, the protocol spec tells us to register as many clients as we need. SSO is the main responsibility of identity provider.
You just need to add RequireConsent = false; to your client def in IdSrv to avoid additional unintended user interaction.
Additionally, nowadays the recommended auth flow for spa-s is "code+pkce". You can take a look at this article in order to get detailed info for transition.

Related

Identity: Propagating claims to downstream services

I am trying to set up a distributed system with Duende-IdentityServer. In my architecture I am using a BFF (Backend For Front) as an API-GateWay for my client.
When my user is logged in using the BFF I want requests to propagate from the BFF to downstream services. (I am using GraphQL with stitching and schema federation, but I feel that might be irrelevant to the question.) Because I feel that it is important for the downstream services to be in control of the authorization of their data I would like claims received by the BFF to be forwarded to the downstream services. I figured something like attaching a JWT Bearer with the claims would work and was hoping that that way my downstream services wouldn't have to contact the identity server to validate the claims.
I tried a few things, but it is quite easy to get lost in the world that is OAuth2 and OIDC. I can't imagine my use case being that
Here is what I tried so far:
In the BFF:
//program.cs
builder.Services.AddHttpClient(GraphQLSchemas.Identity, c => c.BaseAddress = new Uri("https://localhost:7500/graphql")).AddUserAccessTokenHandler();
builder.Services.AddGraphQLServer()
.AddRemoteSchemasFromRedis("GraphQL", sp => sp.GetRequiredService<ConnectionMultiplexer>())
.ModifyOptions(x => x.RemoveUnreachableTypes = true);
services.AddBff();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Bff-Cookie";
options.DefaultChallengeScheme = "oidc";
options.DefaultSignOutScheme = "oidc";
})
.AddCookie("Bff-Cookie", options =>
{
// set session lifetime
options.ExpireTimeSpan = TimeSpan.FromHours(8);
// sliding or absolute
options.SlidingExpiration = true;
// host prefixed cookie name
options.Cookie.Name = bffOptions.Cookie.Name ;
options.Cookie.Domain = bffOptions.Cookie.Domain;
// strict SameSite handling
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = bffOptions.IdentityServer.Host;
// confidential client using code flow + PKCE
options.ClientId = bffOptions.IdentityServer.ClientId;
options.ResponseType = OpenIdConnectResponseType.Code;
options.ResponseMode = "query";
options.MapInboundClaims = false;
options.GetClaimsFromUserInfoEndpoint = false;
options.SaveTokens = true;
//options.
// request scopes +refresh tokens
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
//options.Scope.Add("Administrator");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
options.ClaimActions.MapJsonKey("role", "role", "role");
options.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role;
});
/// code omitted for brevity
app.UseBff();
If I log in on the bff these are the claims I get:
However the access_token doesnt reflect this:
So when my HttpClient uses .AddUserAccessTokenHandler();
Only the access_token is passed to my downstream service:
//program.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = "https://localhost:7500";
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = false,
ValidTypes = new[] { "at+jwt" },
NameClaimType = "name",
RoleClaimType = "role"
};
});
//code omitted for brevity
app.MapGraphQL().RequireAuthorization(new AuthorizeAttribute
{
AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme
}).AllowAnonymous();
But as you can see the Role claim etc is not passed.
How can receive the claims in in my downstream service? Preferably without reaching out to the identityserver. (Though it would be kinda nice if the downstream service could validate the jwt sent to it.
P.S. I also tried to follow 2 tutorials that create a ProfileService implementation, but for some reason when i register another profile service with the DI container the login through the bff fails and I havent been able to figure out why yet. A breakpoint in profile service would not be hit.
The claims you see in .NET is the claims from the id_token or from the userinfo endpoint. They are separate from the ones found in the access token. You configure this in IdentityServer.
See my answer at ApiResource vs ApiScope vs IdentityResource for more details about this.
So, the IdentityResource and ApiResource defines what claims can be returned for a given user. Then as this picture shows, those requested claims, are then looked up against the user database and the claims that is found in the database are then returned and used in the ID and access token. as the picture from one of my training classes shows:
As you may know, Authentication is the concern of the gateway and authorization is Domain-specific and is related to downstream services (each one). JWT token should be validated and verified in the gateway and verification/validation process can be ignored in downstream services.
You can pass JWT headers (Authorization: Bearer bla) to the downstream services and they should ignore the validation like this:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = false;
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = false,
SignatureValidator = (jwt, tokenValidationParameters) =>
{
return new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(jwt);
}
};
});
this code makes all of your routes and HTTP request flows to fullfil User object and its Claims and you can take action like this in your controllers:
//You will have claims in your User object even if you mark the action as anonymous.
//[AllowAnonymous]
public async Task Do()
{
this.User.HasClaim("bla")
}
This is because of the protocol and Dotnet supports JWT out of the box and I highly recommend you to forward JWT-formatted headers and do not change the token format/protocol for downstream services.

identityserver4 local login cause clients logout

User can login to identity server using local account. But this is cause of user sign out from MVC client that uses open id connect for external login and I don't know why exactly!
I checked IdentityServer4 connect/authorize endpoint for any sign out code but I can't find anything.
IdentityServer config:
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
AllowOfflineAccess = true,
RequireConsent = true,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
}
}
Client config:
builder.Services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddAuthentication()
.AddOpenIdConnect("oidc", "Demo IdentityServer4", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("profile");
options.SaveTokens = true;
});
Aren't you by any chance using the same name for the cookies of your MVC client and your identity server?
As you are working in localhost for both MVC client and identity server, the cookie of single sign on could be overwriting the MVC cookie.
Have you checked this behavior with different domains?
And, what if you go back to the MVC Client and login from there, do you lose the single sign on? If I'm right, this time the MVC Cookie would be overwriting the identity server Cookie.
Did you check #IdentiyServer4 logs? I think those errors would help you to find the exact problem.

Pass custom parameter to returnUrl used in login page Identity Server 4

I'm implementing an authentication server with IdentityServer4 for clients using Hybrid flow. I managed to implement my own user store and also my own repository for clients, grants and resources.
When a user wants to login the client redirects it to my authentication server and if it's not loged in, it shows the login page. At this point I need some extra information than username and password in order to login my users. This is a projectId from another system where I'm actually authentication the users to. The client should provide this projectId.
The flow looks like that:
flow
I've read here Sending Custom Parameters to login screen
that I should retrieve parameteres from the returnUrl I get in the AccountController. The way I'm triggering the login flow right now is with the [Authorize] attribute in a controller method in my client code:
[Route("login")]
[Authorize]
public async Task<IActionResult> Login()
My questions are:
1.How can I send the projectId in the connect/authorize request to identity server?
2.Should I create the request manually for that?
2.a If so,then how can I handle the redirect uri action in the controller? Because now i'm using the /signin-oidc standard route.
My client definition looks like that:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5001";
options.RequireHttpsMetadata = false;
options.ClientId = "BGServer";
options.ClientSecret = "ThisIsTheBGServerSecret";
options.ResponseType = "code id_token"; //"code";
//set SaveTokens to save tokens to the AuthenticationProperties
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("BG_API");
options.Scope.Add("offline_access");
});
}
And my client definition in the authentication server looks like that:
// OpenID Connect hybrid flow and client credentials client (BGServerClient)
new Client
{
ClientId = "BGServer",
ClientName = "BabyGiness Server",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("ThisIsTheBGServerSecret".Sha256())
},
RedirectUris = {"http://localhost:5005/signin-oidc"},
PostLogoutRedirectUris = { "http://localhost:5005/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"BG_API"
},
AllowOfflineAccess = true //used to be able to retrieve refresh tokens
};
Thank you very much for your help.
You should be able to simply add additional query string parameters to the authorize endpoint request and then parse them out of the returnUrl in your MVC controller for the login flow. Anything not part of the protocol will be ignored by by IDS4 I'm pretty sure.

IdentityServer4 - missing claims from Google

TLDR; In the context of using IdentityServer4
How do you get email address and hd claims from Google?
How do you get User.Identity.Name to be populated?
I have worked through the IdentityServer quickstarts and have a working MVC client talking to a IdentityServer instance (apologies if using the wrong terminology). I am using External Authentication (Google) and do not have anything mildly complicated such as local logins / database etc. I am not using ASP.NET Identity. This is all working just fine.
I can successfully authenticate in my MVC app and the following code produces the claims in the screenshot below:
#foreach (var claim in User.Claims)
{
<dt>#claim.Type</dt>
<dd>#claim.Value</dd>
}
<dt>Identity.Name</dt>
<dd> #User.Identity.Name</dd>
<dt>IsAuthenticated</dt>
<dd>#User.Identity.IsAuthenticated</dd>
Questions:
I cannot retrieve extra claims (right term?) from Google. Specifically 'hd' or even 'email' - note that they don't show up in the claims in the above screenshot. How do I get the email address and hd claims from Google? What am I missing or doing wrong?
Note that the output of User.Identity.Name is empty. Why is this and how do I get this populated? This seems to be the only property of User.Identity that isn't set.
My setup is as follows - you can see the output of this as above:
Client (MVC)
In Startup.cs, ConfigureServices
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = Configuration["App:Urls:IdentityServer"];
options.RequireHttpsMetadata = false;
options.Resource = "openid profile email";
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("domain");
options.ClientId = "ctda-web";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
Identity Server
Client definition
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "ctda-web",
ClientName = "Company To Do Web App",
AllowedGrantTypes = GrantTypes.Implicit,
EnableLocalLogin = false,
// where to redirect to after login
RedirectUris = { "http://localhost:53996/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:53996/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"domain"
}
}
IdentityResource definition
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "domain",
DisplayName = "Google Organisation",
Description = "The hosted G Suite domain of the user, if part of one",
UserClaims = new List<string> { "hd"}
}
};
The answer is amazingly unobvious: the sample code provided by IdentityServer4 works as long as you have the following configuration (in the Identity Server Startup.cs):
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients()
.AddTestUsers(Config.GetUsers()); //<--- this line here
Why? Because AddTestUsers is doing a bunch of the plumbing you need to do in your own world. The walkthrough implicitly assume you move to EF or ASP.NET Identity etc and make it unclear what you have to do if you aren't going to use these data stores. In short, you need to:
Create an object to represent the user (here's a starter)
Create a persistance/query class(here's a starter)
Create an instance of IProfileService which ties this all together, putting in your definitions of a User and a UserStore (here's a starter)
Add appropriate bindings etc
My IdentityServer Startup.cs ended up looking like this (I want to do in memory deliberately, but obviously not use the test users provided in the samples):
services.AddSingleton(new InMemoryUserStore()); //<-- new
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients())
.AddProfileService<UserProfileService>(); //<-- new
Turns out Google does return email as part of the claim. The scopes in the code sample of the question worked.
I can tell you how to get email to be returned.
There are two ways to do this but they both require that you add the email scope to the initial request. Just sending openId isnt going to work.
Openid email
UserInfo request
Now when you get the access token back you can do
https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token={access token}
Response
{
"family_name": "Lawton",
"name": "Linda Lawton",
"picture": "https://lh5.googleusercontent.com/-a1CWlFnA5xE/AAAAAAAAAAI/AAAAAAAAl1I/UcwPajZOuN4/photo.jpg",
"gender": "female",
"email": "xxxx#gmail.com",
"link": "https://plus.google.com/+LindaLawton",
"given_name": "Linda",
"id": "117200475532672775346",
"verified_email": true
}
Token Info Request:
Using the id token
https://www.googleapis.com/oauth2/v3/tokeninfo?id_token={token id}
response
{
"azp": "07408718192.apps.googleusercontent.com",
"aud": "07408718192.apps.googleusercontent.com",
"sub": "00475532672775346",
"email": "XX#gmail.com",
"email_verified": "true",
"at_hash": "8ON2HwraMXbPpP0Nwle8Kw",
"iss": "https://accounts.google.com",
"iat": "1509967160",
"exp": "1509970760",
"alg": "RS256",
"kid": "d4ed62ee21d157e8a237b7db3cbd8f7aafab2e"
}
As to how to populate your controller i cant help with that.

IdentityServer authentication for admin application in the same host

I have an ASP.NET MVC application for hosting an IdentityServer3, but I want to host Angular + WebAPI 2 custom administration app on that same host. That admin app is using oidc-client library for authentication. Below is my Startup class for configuring IdentityServer and calling UseIdentityServerBearerTokenAuthentication method. As you can see I called that method in a async Task because that happened to soon, before IdentityServer started.
Authentication works, my Angular ajax requests are filled with valid access tokens, but I dont get any Claims on WebApi controllers. ClaimsPrincipal have empty Claims list, and IsAuthenticated is false.
Also my client configuration is properly set. Is there something wrong with this setup?
public class Startup
{
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Trace()
.CreateLogger();
var factory = new IdentityServerServiceFactory();
factory.Register<IdentityDatabaseModel>(new Registration<IdentityDatabaseModel>(typeof(IdentityDatabaseModel)));
factory.Register<UserDataService>(new Registration<UserDataService>(typeof(UserDataService)));
factory.Register<TokenDataService>(new Registration<TokenDataService>(typeof(TokenDataService)));
factory.Register<ClaimsDataService>(new Registration<ClaimsDataService>(typeof(ClaimsDataService)));
factory.Register<ClientDataService>(new Registration<ClientDataService>(typeof(ClientDataService)));
factory.UserService = new Registration<IUserService>(typeof(UserService));
factory.RefreshTokenStore = new Registration<IRefreshTokenStore, RefreshTokenStore>();
factory.ClientStore = new Registration<IClientStore, ClientStore>();
factory.UseInMemoryScopes(WebApplication1.Models.IS.Scopes.Get());
var options = new IdentityServerOptions
{
SigningCertificate = Certificate.Get(),
Factory = factory,
RequireSsl = false,
LoggingOptions = new LoggingOptions
{
//EnableHttpLogging = true,
EnableKatanaLogging = true,
EnableWebApiDiagnostics = true,
WebApiDiagnosticsIsVerbose = true
},
EnableWelcomePage = false
};
app.UseIdentityServer(options);
#region IdentityServer authentication
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
Task.Factory.StartNew(() => {
System.Threading.Thread.Sleep(5000);
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:17343",
RequiredScopes = new[] { "openid", "email", "roles", "profile" },
ClientId = "lsidentity",
ClientSecret = "secret"
});
});
#endregion
}
}
The problem was that I needed to configure IssuerName and SigningCertificate in WebApi configuration, so it looks like this:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:17343",
//Authority = "http://192.168.254.3:303",
RequiredScopes = new[] { "openid",
"email", "profile" },
IssuerName = "http://localhost:17343", //added this
SigningCertificate = Certificate.Get(), // ...and this
// client credentials for the introspection endpoint
ClientId = "lsidentity",
ClientSecret = "secret".Sha256()
});
There was an issue on github, but I did not find it at first.
https://github.com/IdentityServer/IdentityServer3.AccessTokenValidation/issues/38
There is also no need to call this as Task, it works fine now.

Categories