Add multiple external oidc provides to Duende Identity Server - c#

I passed through the tutorial:
https://docs.duendesoftware.com/identityserver/v6/quickstarts/2_interactive/
And in adittional I tried to add another instance of the Identity Server as another external Identity Provider. After this it just stops working right after starting. No errors, no warnings...
Each one works separately.
Whether who faced it?
Here is how I registered several Identity Providers:
builder.Services.AddAuthentication()
.AddOpenIdConnect("oidc", "Demo IdentityServer", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SaveTokens = true;
options.Authority = "https://demo.duendesoftware.com";
options.ClientId = "interactive.confidential";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
})
.AddOpenIdConnect("oidc", "My IdentityServer", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SaveTokens = true;
options.Authority = "https://localhost:5004";
options.ClientId = "myprovider";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});

One problem is that both handlers are listening on the same callback URLs (the URL's IdentityServer sends requests to the client)
by default they are set to
CallbackPath = new PathString("/signin-oidc");
SignedOutCallbackPath = new PathString("/signout-callback-oidc");
RemoteSignOutPath = new PathString("/signout-oidc");
You need to set them to different paths in each handler.
Also, the schema name should be different "oidc"
But in general, I think it's a bad idea to have your client trust two different Identity providers, I think it's better to only have it trust one.
The alternative is to have your own Identity provider locally, that then trusts various external providers, like this:
Generally, your complexity will be reduced if every service only has to trust one provider. Especially, for the APIs using JwtBearer, they prefer to only have one trusted provider.

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.

ASP.NET Core API Controller Action validation of IndentityServer4 client permissions/claims

I have a scenario where multiple clients have access to the same ASP.NET Core API . I.e. the same scopes. They are, however, not permitted to access the same data. If I could identify which client is accessing the API then it could validate whether it had access rights or not. I assume though that this is a bad approach - though doable in my scenario - because it requires code changes when new clients are added. Is there a way to have clients with bearer tokens having claims that I can read in my controller action? How do I configure this is and how do I access the claims in the API?
I am not familiar with IdentityServer so can't answer to the part about adding claims.
However once the user has claims, you can access it in your api quite simply:
[HttpPost("foo/{bar}")]
public async Task<IActionResult> Foo(string bar)
{
var user = User.FindFirst(ClaimTypes.Name).Value;//use whatever kind of claim type you want.
return await DoSomeWork();
}
Hope this helps.
You can add the client id(aud claim in token) to principle in OnTokenValidated event of AddOpenIdConnect extension .
In your MVC client , modify the OIDC configuration :
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.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = ctx =>
{
var clientID = ctx.SecurityToken.Claims.FirstOrDefault(c => c.Type == "aud")?.Value;
var claims = new List<Claim>
{
new Claim("ClientID", clientID)
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
},
};
});
Then you could read the client id in controller like :
var clientID = User.Claims.FirstOrDefault(c => c.Type == "ClientID")?.Value;
With using System.Linq; for FirstOrDefault Linq operation .

Getting the identity token (id_token) within redirect URI (MVC Controller)

I'm hoping this is mostly agnostic from Okta (the service we are using for social logins), but I'm having a hard time finding documentation. I'm using .NET Core 2.0+ and my Startup.cs looks like this:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
//Configuration pulled from appsettings.json by default:
options.ClientId = Configuration["okta:ClientId"];
options.ClientSecret = Configuration["okta:ClientSecret"];
options.Authority = Configuration["okta:Issuer"];
options.CallbackPath = "/authorization-code/callback";
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.SaveTokens = true;
options.UseTokenLifetime = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
});
A login form within the site allows you to click 'Login to Facebook' and the process of authenticating via the identity provider takes place. When it is confirmed, it punts back to my defined redirect 'Home/Secure'. When the redirect returns, the id_token is in the URL as an anchor:
https://localhost:5001/Home/Secure#id_token=XXXXXXX
There is also an authorize call that I can see happen that receives a response with the id_token in it as well via the Chrome developer tools console. I'm not as familiar with .NET Core, so I'm having a hard time understanding how I can grab this id_token.
The Request doesn't seem to have the id_token in the Query or QueryString parameters, so I'm not seeing where I can grab it.
Since you are using the OIDC middleware and set SaveTokens to true , you would subsequently be able to retrieve those tokens by calling GetTokenAsync for ID token you want to access ,in controller :
string idToken = await HttpContext.GetTokenAsync("id_token");

Identity4Server many SPA clients best approach?

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.

How to refresh access token

I have an Asp.net 2.0 core web application which connects to an Identity server 4 application for authentication. There is also an API involved. The API consumes an access token as a bearer token.
My startup:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = idsEndPoint;
options.RequireHttpsMetadata = false;
options.ClientId = "testclient";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("testapi");
});
Controller:
In my controllers i can see my tokens and they are all populated and i can use the access token in my API calls.
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
var refreshToken = await HttpContext.GetTokenAsync(IdentityConstants.HttpContextHeaders.RefreshToken);
var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
Question:
My problem occurs after one hour where the access token expires. It appears that it is not automatically being refreshed. I am wondering if this is a setting in my authentication that will cause it to refresh it. However I have been unable to find out how I am supposed to force it to refresh the access token after it has expired.
My current solution is to refresh it myself but I would have thought this would be built into the cookie middleware.
for automatic refresh token, add options.Scope.Add("offline_access"); to AddOpenIdConnect() options.
This approach uses OpenIddict, you need to implement the main configuration inside startup.cs. The next Link is an excellent example of this implementation. Hope be useful
https://github.com/openiddict/openiddict-samples/tree/dev/samples/RefreshFlow
if (request.IsPasswordGrantType())
{
if (!Email_Regex_Validation.Check_Valid_Email_Regex(request.Username))
{
return BadRequest(Resources.RegexEmail);
}
SpLoginUser stored = new SpLoginUser(_context);
string result = stored.Usp_Login_User(request.Username, request.Password);
if (!result.Contains("successfully"))
{
return Forbid(OpenIddictServerDefaults.AuthenticationScheme);
}
// Create a new ClaimsIdentity holding the user identity.
var identity = new ClaimsIdentity(
OpenIddictServerDefaults.AuthenticationScheme,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
identity.AddClaim(Resources.issuer, Resources.secret,
OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, request.Username,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OfflineAccess);
// Ask OpenIddict to generate a new token and return an OAuth2 token response.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}

Categories