In this article, we are going to learn how to implement the OAuth2 refresh token actions in our Angular application. This action will enable us to silently refresh the access token when it is close to expiry. Also, it will provide a better user experience because the user doesn’t have to manually log in every time the access token expires in our application.
If you want to read the entire IdentityServer4, OAuth2, and OIDC series, feel free to do that and learn a lot more about the application security in ASP.NET Core.
Let’s move on.
More About the Token Management Actions
The OAuth2 access tokens have a fixed expiration time which can lead to some issues while users interact with our application. For example, if our access token’s lifetime is five minutes and the user needs at least 10 minutes to fill out the form on our site, they will receive an unauthorized response from the server on the submit action. That’s because the access token expires and the server can’t authorize the user.
This means we have to redirect the user to the Login screen on the IDP level, and then reload the Angular application. So, unless we preserve the user’s actions somehow locally, the user could lose all their work.
This is not a good user experience at all.
Also, increasing the lifetime of the access token to several days is not a good solution for security reasons. You can read more about that in our article about the refreshing JWT with ASP.NET Core and Angular.
So, what should we do in this case?
About Cookies
Every time the user logs in, the IDP establishes a cookie-based secure session between the user’s browser and the IDP itself. That means when a user sends a request to the server, the session cookie is going to be sent with that request as well. This is a sliding session and it resets the expiration of the cookie. So, if the login session with IDP lasts longer than the lifetime of the access token, the IDP can support the issuing of a new access token, as long as the IDP’s session is still active.
What we need to understand is that using only cookies is not enough. They have some security flaws – XSS (cross-site scripting) and CSRF (cross-site request forgery). On the other side, the tokens don’t have most of the XSS and CSRF vulnerabilities that cookies do. But the cookies can support the sliding expiration in the ways that tokens can’t.
That said, it turns out that the best approach is by combining these two.
In our Angular application, we can do this by sending a request to the /authorize
endpoint with the prompt parameter set to none: /authorize?prompt=none
. This tells the IDP to try to authenticate the user without prompting or trying to show any UI. If the authorization succeeds, the IDP will return a new access token and all we have to do is to replace the original one with a new one. This new token will be used for all the subsequent requests towards the API.
Even though this sounds a bit complicated to implement, it is not, since the oidc-client library does most of the work for us.
Automatic Logout Without OAuth2 Refresh Token Implemented
If we inspect the client configuration on the IDP level, we are going to see the lifetime of the access token set to 600 seconds. For this example’s purpose, let’s lower that value to the 60 seconds:
new Client { ... RequireConsent = false, AccessTokenLifetime = 60 }
Now, you can remove the database and start the IDP application again, or you can just modify the value of the AccessTokenLifetime column inside the Clients
table in your database.
As soon as we log in, we can inspect our access token and see the expiration time of one minute for it:
Let’s improve our application, by showing the Login button, as soon as our token expires.
To do that, let’s modify the constructor of the Auth service:
constructor() { this._userManager = new UserManager(this.idpSettings); this._userManager.events.addAccessTokenExpired(_ => { this._loginChangedSubject.next(false); }); }
With the addAccessTokenExpired
function, we subscribe to an event as soon as the access token expires. There, we just fire the loginChanged
observable through its subject.
In the same way, we are going to modify the finishLogout
action:
public finishLogout = () => { this._user = null; this._loginChangedSubject.next(false); return this._userManager.signoutRedirectCallback(); }
Excellent.
We can log in now and after one minute, we are going to see the Login
link. Also, Companies and Privacy links will be hidden.
Enabling OAuth2 Refresh Token Actions
Right now, we can enable the silent renew of the access token and see it in practice.
The first step we have to do is to modify the configuration in the client application:
private get idpSettings() : UserManagerSettings { return { authority: Constants.idpAuthority, client_id: Constants.clientId, redirect_uri: `${Constants.clientRoot}/signin-callback`, scope: "openid profile companyApi", response_type: "code", post_logout_redirect_uri: `${Constants.clientRoot}/signout-callback`, automaticSilentRenew: true, silent_redirect_uri: `${Constants.clientRoot}/assets/silent-callback.html` } }
We have two new properties. The first one enables the silent renew action and the second one points to the silent renew URI. Pay attention that this URI must be the same as the one in the client configuration on the IDP level:
From this URI, we can see that we require an additional HTML page. So, let’s add that page under the assets folder and name it silent-callback.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=1024, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Silent-Callback</title> </head> <body> </body> </html>
We have to load the oidc-client on this page and we will do that by copying the oidc-client.min.js
file from the node_modules/oidc-client/dist
folder and pasting it in the assets/js
folder:
Then, we have to modify our HTML file:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=1024, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Silent-Callback</title> <script src="/assets/js/oidc-client.min.js"></script> </head> <body> <script> const idpSettings = { authority: "https://localhost:5005", clientId: "angular-client", scope: "openid profile companyApi", response_type: "code" }; new Oidc.UserManager(idpSettings).signinSilentCallback() .catch(error => { console.log(error); }); </script> </body> </html>
Here, we load our script file and create the idpSettings
object with required settings compatible with the Auth service user manager. Finally, we pass the idpSetting
to the UserManager
class and call the signinSilentCallback
function to signin silently.
Testing the Functionality
Now if we start our application and log in successfully, as soon as we inspect the Network tab, we are going to see several calls in the loop:
This happens because the oidc-client triggers the silent renew process 60 seconds before the token expiration. Since our token lifetime is 60 seconds, we are always within the time limit. To solve that, we are going to increase our token lifetime to 120 seconds (InMemoryConfiguration and Database).
Now if we log in, the token and userinfo requests will repeat once per 60 seconds since our token lifetime is 120 and, as we explained, the UserManager triggers the silent renew process 60 seconds before the expiration.
We can inspect the logs to see the call to the /authorize
, /token
and /userinfo
endpoints:
Finally, we can inspect the URI from the request to the /authorize
endpoint:
As you can see, there is the prompt parameter we’ve talked about with the value none.
Also, we are not logged out after the 120 seconds because our token is refreshed.
Cookie Lifetime Setup
If we want to restrict the lifetime of a cookie, we can do that by providing additional options in the AddIdentityServer
method:
services.AddIdentityServer(opt => { opt.Authentication.CookieLifetime = TimeSpan.FromMinutes(4); }) .AddTestUsers(InMemoryConfig.GetUsers()) .AddDeveloperSigningCredential() //not something we want to use in a production environment; .AddProfileService<CustomProfileService>() .AddConfigurationStore(opt => ...
Again, the four minutes lifetime is just for the example purpose.
Now, our application will refresh our token several times every sixty seconds, but after the cookie’s lifetime expires, the user will be forced to log in again.
Conclusion
Great job.
We have learned how to implement the OAuth2 Refresh Token in our Angular application using the oidc-client library and IdentityServer 4. We’ve seen that the process is not that hard because the oidc-client helps us a lot.
Until the next article,
Best regards.
Hi, Thanks for the detailed article. I did implemented this in my project and working perfectly. I have below questions –
Ques – I have idp settings stored in environment file. How can I read it inside silent-callback.html from environment file? In your article, those are hardcoded.
const idpSettings = {
authority: “https://localhost:5005”,
clientId: “angular-client”,
scope: “openid profile companyApi”,
response_type: “code”
};
To be honest I am not sure how to do that. This file is only used for silent callback and thus the configuration was placed there.
Hello @disqus_GtEeIZUUDN:disqus first of all thanks for your effort and the great post, it is really helpful.
Thing that I’m currently using in my project is “opts.Authentication.CookieSlidingExpiration = true” besides “CookieLifetime”, in order to improve user experience. In my understanding, if user interacts with app – every processed request (if life time of cookie is more than a half way expired) is this going to increase cookie lifetime (this is what “CookieSlidingExpiration” does) and this makes perfect sense if that’s just the case for non-refresh token requests. However since this is also the case for SilentRefreshToken request as well (I think?) that makes my accessToken last infinite period of time. Am I missing something obvious? Should “CookieSlidingExpiration” be used with “Silent Refresh Token strategy” in the first place? Thanks for the reply in advance and hopefully what I wrote makes sense to you. If I can clarify anything, please let me know.
To be honest, I am not quite sure wether you should be using these two together. Basically, when you create the silent renew it will renew your token after certain period of time. But you want to do that just a certain amount if time, so setting the cookie lifetime increases the security and forces the user to login again. I know I didn’t answer your question directly, but as much as I know, the technique from this article should be quite good and safe.
@disqus_GtEeIZUUDN:disqus Yeah, that’s exactly my understanding of this topic as well… You basically control “overall token refresh life time” with CookieLifetime setting. Once that expires and you make a new request you would get 401 and redirect user to ‘/unauthorized’ making him relog. That flow makes perfect sense to me. However this is not really the case for me, even tho CookieLifetime expired I can still obtain new tokens with silent renew. Only way for me to control “overall token refresh life time” is by setting “UserSsoLifetime”. Does this makes any sense to you? Any idea how come that even tho CookieLifetime expired Im still able to obtain new token with silent renew?
Just to recap for others – In my experience setting “CookieSlidingExpiration” option and using silent token renew will make access token reissued for infinite period of time, which is probably something we don’t want because of security reasons.
Well, again, I am not sure why is that happening. With this example, it worked for me, and I read in different articles and watched on courses the same implementation, so this is new for me as well.
@disqus_GtEeIZUUDN:disqus I see, ok if I find out what’s going on I’ll just post back here for others to see. Anyways, thank you very much for your time and replies once again!
Hi, Thanks a lot for this course. I have followed you from the start till the end and now i can get my angular application working using identityserver4 and user can login and logout. Be blessed legends!
Two things i have been wondering:
1. The refreshing token working like charm and i set the coockie to x time so the user can automaticaally logout but want i want is the user have to automatically logout if no activities(click events..) have been done on the site for a while. Right now no matter if the user is active or not the coockie just be trigged.
2. In my angular application i am using so i would like to redirect the user after login page to the same url the user was before login(suppose the router.url not require authorization and any user can view the page), not home page as explained in the course. I tried to use router.url to store the current url and when the user get back to the client the window.history.state is null, i guess this is becouse evering get reload after sign-callback. I tried even the state property from usermanager but the state is still null.
Any help will be much appreciated!
Ps: Sorry for my english and I am new in the coding world and just keep learning new stuff!
Hi Joseph. Thanks for reading articles on Code Maze and thank you for the kind words. This really means a lot.
Regarding your questions:
1) To accomplish something like that, you would have to add some custom logic probably, I didn’t try it with this type of setup. Though I don’t recommend that due to security reasons. You should restrict user’s session to hour or two or maybe a bit longer, but never let it be on the site with a same login forever.
2) I have Angular with ASP.NET Core Identity series as well, and in that series I explained the thing you are looking for. You can read more abou it here: https://code-maze.com/angular-authentication-aspnet-identity/ (third article in the series).
For the number one i am keeping it like that, i will do as you said increase users session to maybe two hours or more.
For the segond, the activatedRoute seems not to work when redirect back from idp, but works if i am still in the client app and changing betwwen pages. Anyway i was able to get it work by using sessionStorage and after login i was able to come back to the previous url and not home page, i dont know if it good to do so but i guess it’s just a route stored and i have authorization that would block unaccapted users.
Thanks again,
Well, if you remember the first article, I wrote that our identity server 4 uses the configuration from the database. So if you have modified just the configuration file, this is not enough. You have to delete the entire database and then run the OAuth project once again to seed all the data again to the database.
1000 thanks, Now i remember and got things worked as expeted!
Hi Marinko, I did some changes in my anguar application, one of them is im using router.forroot() in my appModule and router.child() in my appRoutingModule wich handle my few components using router-outlet(Here i use lazy load with comonModule), in the appModule i use BrowserModule and allmost all others module and services. Additional i am using navbar with mat side nav to resize my site based on the screen size. My signin and signout callback components from this course is added in the appModule. Everything works good expect one strange behaviour when loging in, i was not able to getUser() becouse it was undefied but i got the response url with the token when looked at the network tab in console like so: localhost:200/sigin-callback?code….The login button does not change to logout button after redirect and others components as well wich require authorization not being able to be shown. When i copied the signin-call back url with the token and state on it and manually pasted it in the url bar then boom i got my user as expected and then i was able to call signin-callback.component.ts wich i could not before. Why am i not automatically authenticate? I can see quickly after login to idp and redirect the localhost:200/signin-callback?code… being launch but not returning the user. Is something wrong with the way im routing components? Is the signin-callback.component not exixting yet on callback? I am about to manualy retrive th token frm the signin-callback and pass it when redirect finish but that seems unnecessary becouse we did not so in this course.
Thanks,
https://uploads.disquscdn.com/images/7b03d715008f9e276bfd30e9d51bd06dc04a03d090b081d75f29e8f9b1c5cdd3.png
I am really not sure. You will have to play around a bit. Maybe try using Subject in a different way or after some other action to try triggering the manu change. You already have a complex project, it could be anything. There is no way for me to know what is exactly going on there.
Ok, Simple solution for me was to redirect just to localhost:200(No signincomponent) and i app.component call the finishLogin from AuthSerice and check for only user pressed the login buttom otherwiseit will throw no state found. Now after redirect i got the excpected behaviour.
Thanks,
Ok, Simple solution for me was to redirect just to localhost:200(No signincomponent) and i app.component call the finishLogin from AuthSerice and check for only user pressed the login buttom otherwiseit will throw no state found. Now after redirect i got the excpected behaviour.
Thanks,