Blank page after login using bookmarked authorization URL in IdentityServer4 - 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.

Related

What is PostLogoutRedirectUris and signout-callback-oidc in signout flow

what is the purpose of
PostLogoutRedirectUris = { "https://yourclienthost:port/signout-callback-oidc" }
in Client Config please?
When I put it, IdentityServer logout page show confirmed logout message AND shows a link to redirect back to my application.
When I omit it, IdentityServer show confirmed logout message BUT NOT shows a link to redirect back to my application
Regardless of the options, IdentityServer still logs me out. So I am confused what that config does. Please help clarify. Thank you very much!
The purpose of PostLogoutRedirectUris in client config is security.
When the client initiates signout, the client opens a url like that:
https://[identity-server]/connect/endsession?post_logout_redirect_uri=https%3A%2F%2Fclient%2Fapp%2Fsignout-callback-oidc&id_token_hint=XXX&state=YYY&...
Pay attention that the client itself tells IdentityServer where to redirect to after signout. This means that the client might cheat IdentityServer to redirect to a malicious URI.
That's why a white-list of URIs is used on the IdentityServer side, which is called PostLogoutRedirectUris. The list contains all possible deployment locations of the client e.g.
"PostLogoutRedirectUris": [
"https://localhost:44321/signout-callback-oidc",
"https://client/app/signout-callback-oidc", ...
]
When the callback URI suggested by the client is not on the white-list, then
IdentityServer shows confirmed logout message but NOT a link to
redirect back to your application
The signout process continues like that:
.../connect/endsession above redirects to https://[identity-server]/Account/Logout?logoutId=ZZZ
The Logout action renders a page with the link back to the client ( https://client/app/signout-callback-oidc&id_token_hint=XXX&state=YYY&... )
On the same page there is also an iframe wich opens https://[identity-server]/connect/endsession/callback?endSessionId=TTT
This iframe renders another iframe which signs out the client by calling https://client/app/signout-oidc?sid=SSS&iss=https://identity-server
On the same page there is also some JavaScript which automatically opens the link from step 3 and goes back to your client app.
The /signout-callback-oidc route in a MVC client does nothing (probably; I don't know what it does) but it redirects to the home page. (*)
The signout from both IdentityServer and the client app is already complete in step 5. /signout-callback-oidc has nothing to do with the signout itself. It only provides a landing page back in the client app so that the user does not get stuck on a page in IdentityServer.
(*) More precisely, /signout-callback-oidc in the client app redirects to options.SignedOutRedirectUri which can be set during AddOpenIdConnect().
By defaut SignedOutRedirectUri == "/". I prefer to change that option to "/Account/Login". In this way I see the login page right after signout.
"/signout-callback-oidc" is just the default value of options.SignedOutCallbackPath. You can change that path but there is no benefit. This route is automatically registered and automatically handled by the middleware. You cannot write your own controller/action for that route.
PostLogoutRedirectUri is meant to redirect you when you log out of your client. It will, e.g. bring you back to your client application's home screen. When you implement a 'single' logout for your application, you stay logged in to IdentityServer.
Once you are logged out of IdentityServer itself however, no PostLogoutRedirectUri is used and the flow ends on the IdentityServer UI.
In this case, it probably means that you implemented your signout by calling SignOutAsync twice. Once for the local application and once for the IDP but I can't be sure without seeing your code.

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

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

How does AAD API Access delegate permission work?

I'm having a little trouble following how API Access delegate permissions work with azure active directory. I feel like i'm probably misunderstanding a key aspect of how AAD works.
Here is my set up
I have a Web Application let’s call it WebApp. I have created
an AAD for the Web Application and registered with a AAD App ID. Let’s
call it App ID A
I have a Web Api let’s call it ApiService. I have also created an AAD for it and registered with a AAD App ID. Let’s all it App ID B.
In AAD App ID A, I have updated the clicked on the API Access ->
Required Permissions -> Add (App ID B ; Web API) permissions
I’ve updated the manaifest in the AAD App ID B, to give consent to
knownClientApplications to include the client ID of the Web App
I’ve also enable oauth2AllowImplicitFlow to be true for both App’s
manifest.
What I’m trying to do is, A user signs into the web application sign. When it signs in, the user is able to acquire a token for the specific Web App App ID A. The user should be able to use that token and have access the Api Service with App ID B. I thought by configuring the whole API Access -> Required Permissions within the Web Application it would give me delegate permission with the logged in user to communicate with the Api Service WebApi.
When I examine the JWT token, I notice that there is a claim for Microsoft Graph, but not for the ApiService. Shouldn’t I be seeing a claim?
When I try to use the token, it reacts with a 404 authentication error.
Any advice appreciated,
Thanks,
Derek
UPDATE
In response to #joonasw
I actually looked at the example you wrote when i started.
https://joonasw.net/view/aspnet-core-2-azure-ad-authentication
In the example, the web application is initialized with:
.AddOpenIdConnect(opts =>
{
Configuration.GetSection("OpenIdConnect").Bind(opts);
opts.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = ctx =>
{
return Task.CompletedTask;
}
};
});
In the HomeController, there is code to retrieve the token for the graph api
private async Task<string> GetAccessTokenAsync()
{
string authority = _authOptions.Authority;
string userId = User.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier");
var cache = new AdalDistributedTokenCache(_cache, _dataProtectionProvider, userId);
var authContext = new AuthenticationContext(authority, cache);
//App's credentials may be needed if access tokens need to be refreshed with a refresh token
string clientId = _authOptions.ClientId;
string clientSecret = _authOptions.ClientSecret;
var credential = new ClientCredential(clientId, clientSecret);
var result = await authContext.AcquireTokenSilentAsync(
"https://graph.microsoft.com",
credential,
new UserIdentifier(userId, UserIdentifierType.UniqueId));
return result.AccessToken;
}
From my understanding, when the user initially login to the web application it will trigger the OnAuthorizationCodeReceived() method where it will be using the clientId/clientSecret/resource of the web applicaiton. The token is stored in the distributed token cache under the key resource/client id.
In the example, GetAccessTokenAsync() is used to grab the token to access the graph API.
In my case, I was hoping to update that method to retrieve the token for the WebApi which has a different clientId/clientSecret/resoruce. In my case, it will AcquireTokenSilentAsync will throw an AdalTokenAcquisitionExceptionFilter because the token needed is not stored in the cache and in the AdalTokenAcquisitionExceptionFilter it will call try to reauthenticate
context.Result = new ChallengeResult();
which will redirect to the authentication page and then hits the AddOpenIdConnect() method. However, the openIdConnect is configured with the web app clientID/ClientSecret/Resource and will not store the new token properly. It will try to call GetAccessTokenAsync() again and the whole process will go in an infinite loop.
In the example, if you were to comment out the "Anthentication:resource" in app.settings, you will experience the same issue with the infinite loop. What happens is that you initially authenticate correctly with no resource specified. Then when you click on you try to get the token for microsoft graph which is a new resource, it can't find it in the cache and then tries to reauthenticate over and over again.
I also notice that the acquireAsyncAuthentication only returns a AuthenticationResult with a bearer tokentype. How would you get the refresh token in this case?
Any advice?
Thanks,
Derek
UPDATE (Solution)
Thanks to #jaanus. All you have to do is update the resource to the clientid of the web api and pass that into AcquireTokenSilentAsync. The web api id uri that you can get from the azure portal did not work.
Okay, so it seems there are multiple questions here. I'll try to make some sense of this stuff to you.
Adding the "Web App"'s client id to the "ApiService" knownClientApplications is a good idea.
It allows for consent to be done for both apps at the same time. This really only matters for multi-tenant scenarios though.
Now, your Web App will be acquiring access tokens at some point.
When it does, it must specify a resource parameter.
This parameter says to AAD which API you wish to call.
In the case of the "ApiService", you should use either its client id or Application ID URI (this is more common).
Depending on the type of your Web App, the access token is acquired a bit differently.
For "traditional" back-end apps, the Authorization Code Grant flow is usually used.
In this flow your back-end gets an authorization code after the user logs in, and your Web App can then exchange that code for the access token.
In the case of a front-end JavaScript app, you would use the Implicit Grant flow, which you have allowed (no need to enable it in the API by the way).
This one allows you to get access tokens directly from the authorization endpoint (/oauth2/authorize) without talking to the token endpoint as you usually have to.
You can actually get the access token right away after login in the fragment of the URL if you wish.
ADAL.JS makes this quite a lot easier for you if you are going in this route.
The reason you get the authentication error is because the access token is probably meant for Microsoft Graph API. You need to request an access token for your API.
An access token is always only valid for one API.

How can i redirect to an Angular router link with oauth2 login?

I want to make an oauth2 login with Twitch on my website and I have an angular2 website and I'm working with router links.
When I want to log me in with twitch acc to say yes it is me and so everything is fine. Ok the end not xD
When i go to the twitch oauth2 for authorizing i need an redirectUri. My problem is now how can i make this in angular2? Because I can't type www.page.com/app/afterlogin/afterlogin.php or somethink like that.
I need this because I need from the user the access token, I dont want that he need to authorize himself x times.
Maybe this helps for helping me:
https://api.twitch.tv/kraken/oauth2/authorize?client_id=[client_id]&redirect_uri=http://www.page.com/app/AfterLogin/afterlogin.php&response_type=code&scope=user_read
I hope someone can help me with redirecting and some oauth2 logins :)
Let me assume a RESTful backend with Single Page Application front and answer the question. The process in general is like the following
Your SPA --> Your Server --> Your Provider --> Your Browser --> Your Provider --> Your Server --> Your SPA
Your SPA => initializes login and passess redirect_uri
Your Server => Stores redirect_uri in a cookie and sends request to
provider
Your Provider => Gets Success and Failure Urls and loads login page
to your browser
Your Browser => Loads the provider login page
Your Provider => Sends request to your server success or failure
handler
Your Server => Extracts the redirect_uri and redirects the browser
to it
Your SPA => Gets afterLoginUrl from redirect_uri and route the
user to it
Below are the steps to achieve this
When your front end sends the authentication request to your server,
append the redirect_uri. In that url, pass a afterLoginUrl query
parameter. That is used by your front end SPA to route the user to
the specific page that triggered the login. (i.e. If the request has
been triggered by a user trying to access
{base_uri}/profile/project/projects for example, it is a good
practice to route the user to this page rather than to the default
page that a normal login takes to like base_uri/profile/about). As
a result you will have a url that looks like the following.
`http://localhost:8080/oauth2/authorize/google?redirect_uri=http://localhost:4200/oauth2/redirect&afterLoginUrl=/profile/project/projects`
port 8080 being for the back end and 4200 for the front end.
Since you are using a RESTful service, you don't have a way by which you can save the redirect_uri on your server (since REST is stateless). Because of this you need to send it with the request you send to the provider as a cookie.
When the success is received from the provider, you will know which route of your SPA to hit by extracting the cookie you sent. Then you dedicate a route to handle your request from your own server (in my case oauth2/redirect) in your front end app.
On the component specified for the route in step 3 you will receive token and afterLoginUrl(if there is). You will have something like the following on the url
http://localhost:4200/oauth2/redirect?afterLoginUrl=/profile/project/projects&token={token value}
Verify your token, check whether or not there is afterLoginUrl and redirect to the route specified by afterLoginUrl if there is one or to the default profile page if there isn't.
I think a wonderful resource can be found here.
Authorization Code Grant flow is just one of several ways of how you can use OAuth2. It's not suited for applications running in a browser, because it requires a client secret which you cannot keep safe in a browser.
There is another flow - Implicit flow which is meant for JavaScript applications - you get an access token and/or ID token in a redirect URI - in the hash part (#...) so they don't get to a server. Then you can easily use any Angular route path as a redirect URI. So the redirect URL from OAuth2 server could look something like this:
http://example.com/myAngularApp/afterLogin#token=...
When you get to that URI, you just save the token and change the route to some real form.

GWT and AppEngine User Service

I am using GAE User Service to Authrnicate my GWT Application.
Depending on whether the User is logged in the User is presented with LoginPage/Dashboard.
The GWT Application calls a Auth Servlet (Window.Location.assign("/googleauth"); causing application to unload which then transfers control to Google Authentication Page, after authentication we are redirected to CallBack servlet.
I can check whether user is loggedin successfully in Callback Servlet. However if I simply redirect back to my application the session login is lost.
The Application loads from scratch.
If I set up a cookie-->
HttpSession session =
request.getSession();
String sessionid = session.getId(); //Get sessionID from
server's response to your login
request
Cookie cookie=new Cookie("sid",sessionid);
response.addCookie(cookie);
response.sendRedirect(AppURL.getApplicationBaseURL());
In my client code check -->
String sessionID =
Cookies.getCookie("sid");
if(sessionID!=null) { //show
dashboard }
Is the way I am using secure? How long are the cookies valid for?
You said:
I simply redirect back to my application the session login is lost.
This should not happen. Once you login the session should be there until you logout or session timeouts (you can set this in GAE settings).
You can simply make a GWT-RPC call to server and check if user is logged in: UserServiceFactory.getUserService().isUserLoggedIn().
Note: if you are looking for session cookies, AppEngine uses different cookie names in production and development servers. It uses ACSID cookie in production and dev_appserver_login.

Resources