IdentityServer4 - What is the difference between 'RedirectUri' and 'ReturnUrl' - identityserver4

I'm new to both Identity Server and OAuth 2.0 and am struggling to understand what is happening with each network request after I login.
I have set up a very basic authorization code grant login process with identity server that results in the following series of network requests...
The user attempts to access a resource with the [Authorize] attribute on the client (/home/secret)
They are redirected to the login screen
They login
They can now access the protected resource
I'm struggling to understand what is happening at the point the callback url is triggered.
I understand that redirect_uri is an OAuth term that refers to an address on the client the authorization server (identity server in this case) will send the authorization code to (setup against a client with the setting RedirectUris). This accounts for the signin-oidc request above...
...but what about callback? This url appears to be stored as a ReturnUrl parameter after the user is challenged and redirected to the login page.
What is the difference between this ReturnUrl and the standard OAuth redirect_uri?
In OAuth tutorials I've looked at, they describe the key exchange process as follows...
Authorization server checks username and password
Sends authorization_code to redirect_uri
authorization_code, client_id and client_secret sent back from client to authorization server
authorization_code is checked. access_token sent to redirect_uri
access_token is used to access protected resource
I'm struggling to map this process to what Identity Server appears to be doing.
Any help would be much appreciated!

To understand the need for this parameter we must take into account that in the authorization endpoint we can obtain different types of interactions with the user and that they can also be chained one after the other before being redirected to the client application. For example, an interaction could be the presentation of a Login view and, after a successful sign-in, a Consent screen.
User interactions
The conditions of the request will be evaluated to determine the type of interaction that must be presented to the user. These are some of the possible interactions when you access to the /authorize endpoint:
The user must be redirected to the Login view.
The user must be redirected to the Consent view.
The user must be redirected to the Access Denied view.
The request prompt parameter is none but the user is not authenticated or is not active. A login_required error will be returned.
The request prompt parameter is none, consent is requires but there isn't consent yet. A consent_required error will be returned.
...
And these are some of the conditions to determine which interaction should be used:
Prompt mode (login, none, consent)
If the user is or not authenticated
If user is or not active
If client allows or not local logins
...
Where does the url parameter come from
When you configure IdentityServer4 you can configure the name of this parameter by setting the option UserInteraction.LoginReturnUrlParameter. It is also possible to configure the path of the login endpoint:
services.AddIdentityServer(options =>
{
options.UserInteraction.LoginReturnUrlParameter = "myParamName"; // default value = "returnUrl"
options.UserInteraction.LoginUrl = "/user/login"; // default value = "/account/login"
})
And the value of this parameter is the constant value AuthorizeCallback composed of the Authorize constant and "/callback":
public const string Authorize = "connect/authorize";
public const string AuthorizeCallback = Authorize + "/callback";
Where is this parameter used?
In your case your /authorize endpoint has resolved to redirect to the login view. Notice the returnUrl paremeter added to the login url:
/login?ReturnUrl=/connect/authorize/callback
We can see that this parameter is used in the model of the login view. The following code is extracted from the UI proposed by IdentityServer4 Quickstart.UI:
Login (get). Shows the login page
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl); // returnUrl is added to the LoginViewModel.ReturnUrl property
...
return View(vm);
}
Login (post). Validate credentials and redirect to returnUrl:
[HttpPost]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
...
if (_users.ValidateCredentials(model.Username, model.Password))
{
...
await HttpContext.SignInAsync(isuser, props); // Creates the "idsrv" cookie
...
return Redirect(model.ReturnUrl); // If we come from an interaction we will end up being redirected to ReturUrl
...
}
}
But I guess your question is why doesn't the idp just redirect to the client application using the redirectUri (signin-oidc) instead of using a local returnUrl?
IdentityServer will receive the request again to re-evaluate if other interaction is necessary before redirect to the client application, for example a consent interaction. Successive requests will be authenticated since we already have a session cookie.
These callbacks to /authorize/callback will take into account additional conditions to the previous ones to determine the next interaction, for example:
If there was already consent or not
If user accepted consent
If user denied consent
...
And what about the original url? (your /home/secret)
This time we are in your client app...
The authentication middleware keeps the original request data in a HttpContext.Feature, this way it will be available later in the handler.
context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = context.Request.Path,
OriginalPathBase = context.Request.PathBase
});
The base class AuthenticationHandler retrieves this feature in two variables:
protected PathString OriginalPath => Context.Features.Get<IAuthenticationFeature>()?.OriginalPath ?? Request.Path;
protected PathString OriginalPathBase => Context.Features.Get<IAuthenticationFeature>()?.OriginalPathBase ?? Request.PathBase;
When the user attempts to access a resource with the [Authorize] attribute on the client (/home/secret) What it does is invoke the challenge action for your remote handler.
This is the code for the challenge action for OpenIdConnectHandler. This code of the challenge is very similar in other handlers: OauthHandler, CookieAuthenticationHandler.
if (string.IsNullOrEmpty(properties.RedirectUri))
{
properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
}
So, the original path "/home/secret" is stored in the AuthenticationProperty.RedirectUri. This AuthenticationProperty is encoded in the state parameter to be sent to the /authorize endpoint.
OAuth2 establishes that if the identity provider receives this parameter, it must return the same value in the response:
state
REQUIRED if the "state" parameter was present in the client
authorization request. The exact value received from the
client.
The challenge action redirects to the idp /authorize endpoint with the OAuth2 parameters including state.
When the idp interaction finish it will redirect us back to our client application with the autentication response, which includes the state value.
The remote handler captures this callback (/signin-oidc), decodes the AuthenticationProperties and invokes the SignIn action of the SignIn scheme.
The SignIn scheme is configured in the handler options SignInScheme property or, if not configured, it will use the default scheme for this action (usually "Cookies").
The SignIn invokation occurs in the abstract base class RemoteAuthenticationHandler, base class of all remote handlers. Notice the parameter ticketContext.Properties. This properties include our original url in the RedirectUri property:
await Context.SignInAsync(SignInScheme, ticketContext.Principal, ticketContext.Properties);
The Cookie handler SignIn action creates the session cookie for our client application (it should not be confused with the SSO cookie created in the idp "idsrv") and uses the RedirectUri property to redirect to the original path: /home/secret

Related

Springboot + JWT +OAuth2 + AngularJS Stateless session

I am trying various Java Spring based security implementations as follows
1. JWT Authentication
User access /
Springboot identifies as protected resource and redirects user to /login
User enters credentials and browsers does a POST to /authenticate
Server validates the credentials and generates JWT token. Set into response header and redirects to /
Browser loads /. AngularJS recognizes JWT token in the response header and stores the same in the localStorage
All subsequent calls will have the Bearer token in header (injected through httpInterceptor)
Note: Stateless Session
2. OAuth2 authentication
User access /
Springboot identifies as protected resource and redirects user to /login
/login is intercepted by Spring security. Redirects to Oauth2 authorization server with a generated state and redirect URL back to application
User enters credentials
Oauth server redirects back to application URL "/login?code=xxx&state=yyy"
/login is intercepted by Spring security. Recognizes the code and state, generates Cookie and sets in response header. Redirects to /
Browser loads /. Browser recognizes cookie in the response header and stores the same.
If a call is made to /user, the Principal object is populated with the JWT which I am able to extract as follows
#RequestMapping(value= {"/user")
public ResponseEntity<Map<String, String>> user(Principal principal) throws Exception {
OAuth2Authentication obj = (OAuth2Authentication) principal;
authentication = obj.getUserAuthentication();
OAuth2AuthenticationDetails oAuth2AuthenticationDetails = (OAuth2AuthenticationDetails) obj.getDetails();
String jwt = oAuth2AuthenticationDetails.getTokenValue();
All subsequent calls will have the Cookie in the Request
Note: A Stateful Session is created in server side to store the session details. This required to decrypt the cookie and identify the user
Now I want to implement security using Oauth2+JWT but stateless at same time as follows
3. OAuth2 + JWT + Stateless
User access /
Springboot identifies as protected resource and redirects user to /login
/login is interecepted by Spring security. Redirects to Oauth2 authorization server with a generated state and redirect URL back to application
User enters credentials
Oauth server redirects back to application URL "/login?code=xxx&state=yyy"
/login is intercepted by Spring security. Recognizes the code and state, extract JWT token by invoking
OAuth2AuthenticationDetails.getTokenValue() and set in response
header. Redirect to /
Browser loads /. AngularJS recognizes JWT token in the response header and stores the same in the localStorage
All subsequent calls will have the Bearer token in header (injected through httpInterceptor)
Question
I am trying to figure out how to implement the highlighted step above
Just an idea/direction, if I got you right:
You can create a GenericFilterBean and add that to the HttpSecurity filter chain.
When using JWT, there should be something similar (a filter, which extracts the bearer-token from the header) and then populates an Authentication object for Spring Security.
So the new filter could grab the token from the request and set the response accordingly.
You could also handle that in an unprotected (!) callback endpoint like login/callback?..., which than sets the cookie for you.
In our application, the server (spring boot) is totally stateless and does not have any oauth nor stateful stuff. Obviously it never redirects anything or has any other views/endpoints than / for AngularJS (and some REST-APIs under /api/...). Thus, the OAuth-flow is totally handled by AngularJS, which in turn checks the callback from the oauth-server and locally sets the JWT-Token (like in your first approach). In first versions we also tried to mix up redirects with stateless JWT and stateful sessions etc., but this led to very strange behavior with the logins - the state (logged in or not) was not always clear and in some cases redirects were wrong etc.
This might help you implement your desired solution.
The author proposes that once a user successfully authenticates with Oauth2 providers (Google etc), you send a short-lived token as a URL param to your frontend application and use this short-lived token to exchange it for a longer-lived token.

IdenityServer4 - doesn't redirect after MFA

My Auth Server uses IdentityServer4.
Redirect configured as follows for a client
RedirectUris = new List<string>
{
"https://localhost:44342/signin-oidc"
}
this works fine for those users for whom MFA is not enabled. But when it is enabled, and kicks in, the redirect doesn't work. After successful 2nd FA, user stays back on the AuthServer page.
Any idea why?
Multifactor authentication is not implemented by Identityserver4. Identityserver4 is about how the third party application gets access to protected resources on behalf of the user.
The means of how the user gets authenticated are out of the identityserver4 scope. In other words, this is not related to identityserver4.
If you're using the identityserver4 quickstart it comes with ASPNET Identity, ASPNET Identity provides you with a local authentication system for ASPNET applications. MultiFactor Authentication is probably there.
Being said that, when you try to POST to the /authorize endpoint (note authorize not authenticate) from your client application IdentityServer tries to authorize your request and to do so it makes you authenticate first, by presenting you the Login Form.
If you look at the Address bar on this point, you'll notice there's an encoded url as returnUrl param, on the controller code you'll see a check that if that param is present, redirect to that url after successful login.
So, check the flow on your application and see where does that parameter get lost on the redirect hell, at some point you're not passing the returnUrl.

Identity Server 4 - federated logout of google when used as an idp

I have google configured as an external identity provider. How do I configure IdentityServer to also log out of this external identity provder as well as all my client applications?
FYI, the client application sign out is already working. Just want to log user out of google as well.
Anwer
Google doesnt supprot it Signout for Google External Identity Provider isn't working
Background Information:
When a user is signing-out of IdentityServer, and they have used an external identity provider to sign-in then it is likely that they should be redirected to also sign-out of the external provider. Not all external providers support sign-out, as it depends on the protocol and features they support.
To detect that a user must be redirected to an external identity provider for sign-out is typically done by using a idp claim issued into the cookie at IdentityServer. The value set into this claim is the AuthenticationScheme of the corresponding authentication middleware. At sign-out time this claim is consulted to know if an external sign-out is required.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId);
var user = HttpContext.User;
if (user?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await HttpContext.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(user.GetSubjectId(), user.GetName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
Ripped directly from the documentation Sign-out of External Identity Providers
This is what I did. On the logged out view, I added an iframe and a button, that when clicked loads the google logout url in the iframe. This seems to be working well.

Blank page after login using bookmarked authorization URL in IdentityServer4

We have discovered that our users very often for the first time visits our web application by browsing the direct URL of the OIDC client (https://oidcclienturl.com/), The ASP.NET Core OIDC authentication middleware kicks in and the user gets redirected back to Identityserver 4 login page.
Everything works fine but then they decide to add the (temporary? state, nonce, cookies...) authorization URL as a bookmark in their browser before entering their credentials and continuing back to the web application.
This causes an issue when the user later uses the bookmark in a new session. The login seem to actually work after entering valid user credentials even if the user uses an old authorization URL, but when the user gets redirected back to the web application they end up on a blank page (https://oidcclienturl.com/signin-oidc).
After the blank page have been loaded the user is able to browse the direct URL (https://oidcclienturl.com/) sucessfully and appear as an authentcated user in the web application.
Any ideas whats causing the blank page?
That blank page shouldnt exist, if I understand it correctly its the default callback path of the oidc authentication middleware in ASP.NET Core.
Unfortunately, the real-world problem of users bookmarking the login page isn't handled cleanly by OIDC, which requires the client app to initiate the login flow.
I've addressed this by adding a RegistrationClientId column to my user data table, which is the Identity Server ClientId corresponding to the client app that called IDS when the user account was created. In the client app configuration, we use the custom Properties dictionary to add a URI fragment:
new Client
{
ClientId = "some_client",
ClientName = "Some Client",
ClientUri = "https://localhost:5000",
Properties = new Dictionary<string, string>
{
{ "StartLoginFragment", "/Auth/StartLogin" }
}
// other config omitted
};
When a user logs in, an empty return URL indicates IDS wasn't called by a client app, so we use RegistrationClientId to query IClientStore, then we combine the ClientUri and StartLoginFragment URIs and use the resulting URI to redirect the user back to the client application.
Over in the client application, that endpoint kicks off the OIDC sign-in flow, and since the user is already signed-in on IDS, it comes right back to the correct location in the client app. The controller action looks like this:
[HttpGet]
public async Task StartLogin()
{
await acctsvc.SignOutAsync();
await HttpContext.ChallengeAsync("oidc",
new AuthenticationProperties()
{
RedirectUri = "/"
});
}
The call to SignOutAsync just ensures any client-app signin cookies are cleaned up. It's in our custom account service, but it just runs HttpContext.SignOutAsync on the usual "Cookies" and "oidc" schemes. Normally that would also result in a signout call to IDS, but the redirection by the subsequent ChallengeAsync replaces the pending signout call.
The downside is that the action is an HTTP GET meaning pretty much anyone could theoretically trigger this action. At most it would be an annoyance.
In the special case where your IDS is only handling auth for a single client, you can skip a lot of that -- if they land on the page with no return URL, just send them to your client app start-login endpoint straightaway, before they login.

signin from external (but trusted) clients in identity server 4

I had a requirement that the user could enter his password and username directly on the client to sign on.
Without much trouble i just created a very simple extra authenticate action within the same application as identity server that looks like the following.
public async Task<IActionResult> AuthenticateUser(
[FromBody] LoginInputModel model,
[FromServices] AzureB2CUserService userService
)
{
var context = new ResourceOwnerPasswordValidationContext { Password = model.Password, UserName = model.Username };
await userService.ValidateAsync(context);
if (context.Result.Subject != null)
{
AuthenticationProperties props = null;
// only set explicit expiration here if persistent.
// otherwise we reply upon expiration configured in cookie middleware.
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
// issue authentication cookie with subject ID and username
// var user = _users.FindByUsername(model.Username);
await HttpContext.SignInAsync(context.Result.Subject.GetSubjectId(), model.Username, props, context.Result.Subject.Claims.ToArray());
return Ok(context.Result);
}
ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
return BadRequest(ModelState);
}
from the single page application I then call this action and if success, i know that a local authentication cookie has been for identity server.
Then i do a .userManager.signinSilent() from the oidc-client and since the cookie is there, it will get a token exactly the same way if I had used an implicit grant with userManager.signInRedirect but without the user getting redirected.
Is there something I should be aware of from a security point here. (You may assume that cross site attacks and antiforgery tokens have been handled).
instead of callign the silent signin after, could I just do a redirect to the implicit flow in the custom authenticate method and have it end up again with the client application ?
Is there something I should be aware of from a security point here.
(You may assume that cross site attacks and antiforgery tokens have
been handled).
My understanding is that you (1) forfeit the ability to participate in Single Sign-on since your browser does not redirect to the SSO Authority, and (2) introduce a weakness in password handling since your client app (both JS/C# in this case) see the password in the plain text.
instead of callign the silent signin after, could I just do a redirect
to the implicit flow in the custom authenticate method and have it end
up again with the client application ?
If you did this, then you would essentially have the authorization_code flow without the authorization code. Might as well just upgrade to the higher security of that flow.
Assuming you have a SPA on top of an ASP.NET MVC app, you could use a traditional MVC form post with a redirect to SSO and then, upon return, spin up the SPA.

Resources