I'm doing a proof of concept, introducing OpenId Connect to our suite of products. After much discussion, we decided it would probably be better to have a separate, discreet Javascript application (Vuejs) for each product, rather than include them all in one monolithic SPA.
For this to work, we effectively need Single Sign-On ("SSO"). I have been using IdentityServer 4 as our IDP.
I seem to have had some reasonable success by providing multiple uris to the RedirectUris property of the relevant Client config of the Idp. So, both apps are effectively the same Client.
That does not feel right and I want to verify whether I am doing that correctly.
Here is my config for the Client config that covers the 2 SPAs (that would cover all the SPAs).
new Client
{
ClientName = "Some App",
ClientId = "appsuiteid",
AccessTokenLifetime = 330,// 330 seconds, default 60 minutes
IdentityTokenLifetime = 300,
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Sliding,
UpdateAccessTokenClaimsOnRefresh = true,
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
AllowAccessTokensViaBrowser = true,
RedirectUris = new List<string>
{
"http://localhost:8080/authcallback.html", <-- first 2 Uris for first SPA
"http://localhost:8080/silent-refresh.html",
"http://localhost:8090/authcallback.html", <-- second 2 Uris for second SPA
"http://localhost:8090/silent-refresh.html",
},
PostLogoutRedirectUris = new List<string>
{
"http://localhost:8080/signout-callback-oidc",
"http://localhost:8090/signout-callback-oidc",
},
AllowedCorsOrigins = new List<string>
{
$"http://localhost:8080",
$"http://localhost:8090",
},
AllowedScopes = new List<string>
{
IdentityServer4.IdentityServerConstants.StandardScopes.OpenId,
IdentityServer4.IdentityServerConstants.StandardScopes.Profile,
},
ClientSecrets = { new Secret("somepwd".Sha256())}
}
Note, if I configure a separate client for each SPA, it does not work as each SPA gets redirected to the distinct consent page for that client - thus losing the SSO characteristic which we require.
Any guidance on this would be great.
Thanks
Related
I have an Identity Server 4 instance running at https://localhost:5443/ and a client React.js application running at http://localhost:3000/ and making a reference to the oidc-client library in order to establish the communication. I've been following more or less this article.
The way I've configured the client (in-memory) on the Identity Server is as follows:
new Client
{
ClientId = "react-js-implicit-flow",
ClientName = "Some App name",
ClientUri = "http://localhost:3000",
AllowedGrantTypes = GrantTypes.Implicit,
RequireClientSecret = false,
RedirectUris = { "http://localhost:3000/signin-oidc", },
PostLogoutRedirectUris = { "http://localhost:3000/signout-oidc" },
AllowedCorsOrigins = { "http://localhost:3000" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"weatherapi.read"
},
AllowAccessTokensViaBrowser = true
}
and the way it looks like on the Ract.js app is like this:
In general, everything goes well. I can login and logout from the Identity Server but the issue is that here:
I get no value (it is null) and this stops the Identity server from redirecting me back to the client application right after logout. If I hard code it (http://localhost:3000/signout-oidc) it works. But for some reason it is just not available.
During the logout this is what the Identity Server logs show:
So, no error, no nothing but I still can not navigate back to the client app after logout.
You do not provide an idTokenHint (id token) with your logout request like the following:
const mgr = new Oidc.UserManager(settings);
const signoutUrl = await mgr.createSignoutRequest({id_token_hint: user.id_token});
window.location.href = signOutUrl.url;
//or
await mgr.signoutRedirect({id_token_hint: user.id_token});
//or just
await mgr.signoutRedirect();
//it will try to attach id_token internally
Lack of the token is the reason for Identityserver to skip the post_logout_redirect_url parameter.
Identityserver has to validate the parameter against the client's configuration, but without the token it can't.
What solved the issue for (me thanks to answer by #d_f) was to change something on the client side and more specifically: src/services/userService/signoutRedirect.
I followed this guide to implement some kind of authentication for my Blazor Web Assembly application. I have an Identity Server 4 instance running on some server, it seems to be completely operational.
My problem is that in the guide above, the returnUrl that is passed to identity server is obviously not a local url. By digging into Identity Server's code, I found that it will always fail to login a user if the return url is not local :
public async Task<AuthorizationRequest> GetAuthorizationContextAsync(string returnUrl)
{
var result = await _returnUrlParser.ParseAsync(returnUrl);
if (result != null)
{
_logger.LogTrace("AuthorizationRequest being returned");
}
else
{
_logger.LogTrace("No AuthorizationRequest being returned");
}
return result;
}
In the code above from DefaultIdentityServerInteractionService, ParseAsync() calls IsLocal() which causes result to be null, which in turn, produces the following in my logs:
2020-09-28T19:37:25.932782009Z [2020-09-28T19:37:25.9324455+00:00] [VRB] [] [IdentityServer4.Services.OidcReturnUrlParser] returnUrl is not valid
2020-09-28T19:37:25.932807561Z [2020-09-28T19:37:25.9325314+00:00] [VRB] [] [IdentityServer4.Services.OidcReturnUrlParser] No AuthorizationRequest being returned
2020-09-28T19:37:25.932817324Z [2020-09-28T19:37:25.9325559+00:00] [VRB] [] [IdentityServer4.Services.DefaultIdentityServerInteractionService] No AuthorizationRequest being returned
Could someone point me towards what I'm not understanding here ? Can I provide any more information ?
I'm not sure if your issue is about LocalUrl, it seems that you set it wrong if you followed the guid.
If you are following default settings, its enough to have client config on IDS4 like this:
new Client
{
ClientId = "wasmappauth-client",
ClientName = "Blazor Webassembly App Client",
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
AllowedCorsOrigins = { "http://localhost:5005" },
RedirectUris = { "http://localhost:5005/authentication/login-callback" },
PostLogoutRedirectUris = { "http://localhost:5005/authentication/logout-callback" },
AllowedScopes = {"openid", "profile"},
}
Here is my blog post which I explained same thing but in simpler wording: https://nahidfa.com/posts/blazor-webassembly-authentication-and-authorization-with-identityserver4/
Edit: About check for local URLs, its it by design on IDS4
I'm using resource owner password grand type. I implemented 2 interfaces: IResourceOwnerPasswordValidator and IProfileService. When I request a token, I get to the ResourceOwnerPasswordValidator, after successful validation I expect ProfileService provides me with requested climes. But it never does.
The thing is the RequestedClaimTypes from ProfileDataRequestContext in the ProfileService is always empty, as a result I can't filter out claims which weren't asked.
I expect it contains all claims, which are related to the requested scopes.
For example if I request such scopes as email or "profile" then I expect claims like "email", "first_name", "preferred_username" and others to be in the RequestedClaimTypes list.
There is simple clients and identity resource config:
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
...........................................................................
new Client
{
ClientId = "client",
ClientName = "SomeClient",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowOfflineAccess = true,
UpdateAccessTokenClaimsOnRefresh = true,
RefreshTokenUsage = TokenUsage.ReUse,
SlidingRefreshTokenLifetime = 1200,
RefreshTokenExpiration = TokenExpiration.Sliding,
AccessTokenLifetime = 900,
IdentityTokenLifetime = 120,
AllowedGrantTypes = { GrantType.ResourceOwnerPassword },
AccessTokenType = AccessTokenType.Jwt,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
}
},
Interesting fact is that RequextedResource from ProfileDataRequestContext has claims data, which I can use to figure out what claims user has requested. For example:
var claimsNames = new List<string>();
claimsNames.AddRange(context.RequestedResources.IdentityResources.SelectMany(r => r.UserClaims));
claimsNames.AddRange(context.RequestedResources.ApiResources.SelectMany(r => r.UserClaims));
context.RequestedClaimTypes = claimsNames;
But it seems to be hacky way. Does someone know why RequestedClaimTypes isn't populated automatically?
P.S Client is the postman with the following parameters
I have an application that authenticates with Identity Server 4 using implicit flow. The application is separated into two separate .net core applications. One application handles the back end and the other handles serving the front end.
The back end and front end share the same scopes, api name, and authority settings.
I will be writing an integration with a 3rd party. Our back end application will call into the 3rd party application. We need to ensure that calls to the 3rd party application are authenticated. I'd like to share the access token the back end application receives from the front end and send that to the 3rd party application. I am not quite sure of the setup required to do this.
I thought that I could add a new Client to Identity Server and set it up with only the needed scopes required by the 3rd party. But in my local testing, I haven't been able to get this to work. I get an error IDX10804 Unable to obtain configuration from .../.well-known/openid-configuration - A security error occurred.
My config looks like:
new Client
{
ClientId = "thirdPartyClient",
ClientName = "thirdPartyClient",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
},
AllowOfflineAccess = true,
AllowAccessTokensViaBrowser = true,
RequireConsent = false
},
new Client
{
ClientId = "myapplication",
ClientName = "myapplication",
AllowedGrantTypes = GrantTypes.Implicit,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "https://.../callback.html" },
PostLogoutRedirectUris = { "https://.../index.html"
AllowedCorsOrigins = { "https://..." },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Address,
"myscope",
},
AllowOfflineAccess = true,
AllowAccessTokensViaBrowser = true,
RequireConsent = false
}
Am I going about this the right way?
You haven't added redirect uri for the client thirdPartyClient. Without redirect uri client won't be valid.
RedirectUris
Specifies the allowed URIs to return tokens or authorization codes to
If you turn on detailed logging with Disable Just My code. You could see the error logs to find the actual reason
What is the mechanism for setting a token validation period in Identity Server 4? Can the validation period vary for different tokens?
The Identity Server 4 documentation at http://docs.identityserver.io/en/dev/ shows a AuthorizationEndpoint with a property max_age, which is what I think I want, but the documentation does not really show it it inter-operates with the quickstart code for IdentityServerWithAspNetIdentity.
I have modified the Client object in Config.cs of the IdentityServerWithAspNetIdentity
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("LynxJournal".Sha256())
},
//RedirectUris = { "http://localhost:5002/signin-oidc" },
//PostLogoutRedirectUris = { "http://localhost:5002" },
RedirectUris = { serverConfig["MvcClientUrl"] + "/signin-oidc" },
PostLogoutRedirectUris = { serverConfig["MvcClientUrl"] },
IdentityTokenLifetime = 3600,
AccessTokenLifetime = 3600,
AuthorizationCodeLifetime = 3600,
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
"api1"
}
}
This extends the life of the token to one hour where before the defaults seemed to give about 15-20 minutes. I added values for IdentityTokenLifetime, AccessTokenLifetime and AuthorizationCodeLifetime
Token lifetimes are set per client application. This includes both identity and access tokens. See client application entity.
If you are talking about session length this is set by each application upon successful authentication using IdentityServer.