How to secure your web application with the Meecrogate OAuth2 server
Context
the Meecrogate environment is available so we want to authenticate the users of our frontend application through the Oauth2 server. We will be focusing on how to implement the front end application with Angular. The first step would be to register our client on the Oauth2 server and specify the flow we are interested in. The Oauth2 server offers few flow options:
-
authorization code
-
PKCE
-
client credentials
-
password
-
refresh token
You can find more information about those different flows in here.
For this example we will register our client with 'Authorization Code with PKCE' type, so from the front end we will have the following steps to manage:
-
go to the login page (customizable) on the oauth2 server with a return URL
-
catch the code return when the flow come back to the return URL from the login page
-
ask for a JWT token with the returned code
-
use the token to enrich your request to the APIs
-
refresh when/if needed
Typescript Example
This example has been implemented with Angular so it is in typescript and can be adapted for react or other frameworks.
login page
At the begining either we check if the user isn’t authenticated then we directly go to the Oauth2 login page or we can also have a non-logged user access with a login button.
startLogin(returnTo: string) {
// create and store a code challenge/verifier
const codeVerifier = [...Array(30)].map(() => Math.random().toString(36)[2]).join('');
localStorage.setItem('meecrogate_code_verifier', codeVerifier);
// store the returnTo URL
if (returnTo) {
localStorage.setItem('meecrogate_return_url', returnTo);
}
// encore de code verifier and call the authorization server
crypto.subtle.digest('SHA-256', new (<any>window['TextEncoder'])('utf-8').encode(codeVerifier))
.then(hash => encodeURIComponent(Base64.encode(hash)))
.then(challenge => window.location.href = environment.meecrogateOauth2Server + `/api/security/authorize?response_type=code&client_id='+environment.clientId+'&redirect_uri='+returnTo+'&code_challenge=${challenge}&code_challenge_method=S256`);
}
The method above creates a codeVerifier and stores it at the browser level, it also stores the returnUrl. The user is then sent to the login page with all those information.
The design of the login page can easily be customized through the oauth2 admin portal when you create the client.
from the login page to the returnURL
On our angular application we have a method that will extract the code when the user comes back from the login page to the returnUrl page.
onCode(code: string) {
// create code verifier
const codeVerifier = localStorage.getItem('meecrogate_code_verifier');
// create the form with all needed information
const form = new URLSearchParams();
form.set('grant_type', 'authorization_code');
form.set('code', code);
form.set('code_verifier', codeVerifier);
// push the requets to the authorization server
return this.http.post(environment.meecrogateOauth2Server +'/api/security/token', form.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
}).pipe(
map(token => this.onToken(<Token>token)),
finalize(() => localStorage.removeItem('meecrogate_code_verifier')),
catchError(this.errorService.handleError<any>('getToken')));
}
We catch the code returned and request for a JWT token.
JWT token
Let’s have a look at the token before managing it. the token can easily be customized and enriched with other data (e.g. LDAP information …) but in our case we are mainly interested by the token itself so we can consume protected apis.
{
"access_token": "eyJhb...",
"token_type": "Bearer",
"expires_in": 3600,
...
}
We are going to store the token at the service level as well as the expiry information so we can refresh the token when needed.
onToken(token: Token) {
// store the token at service level
this.token = token;
this.userMetadata = this.token.token_type == 'Meecrogate-No-Security' ?
{} :
JSON.parse(atob(this.token.access_token.split('.')[1]));
// store expire At information
this.userMetadata.$expiresAt = this.token.expires_in > 0 ? new Date().getTime() + (1000 * this.token.expires_in) : -1;
// forward the user to the stored returnedUrl
const returnUrl = localStorage.getItem('meecrogate_return_url');
if (returnUrl) {
localStorage.removeItem('meecrogate_return_url');
this.router.navigate(returnUrl.split('/'));
}
return token;
}
Now we can call the apis with our token so the user can navigate in our application. We only need to enrich the header of the apis calls with the token information
enrich header request with the token
Now that we have a token we can call the protected apis with it and eventually renew the token when it get expired.
We first create an interceptor to catch all the outgoing requests
@Injectable()
export class HttpTokenInterceptor implements HttpInterceptor {
constructor(private router: Router, private loginService: LoginService) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.loginService.customizeheaders(req).pipe(
switchMap(request => next.handle(request)),
catchError(error => {
if (error.status === 401 || error.status === 403) {
this.loginService.startLogin(undefined); // sent the user back to the login page
} else if (error.status === 404) {
this.router.navigate(['Home']);
return observableThrowError({ error: 'unable to find this page' });
} else if (error) {
return observableThrowError(error);
}
}));
}
}
the intercepted request will be enriched with the token information but we also check if the token is still valid otherwise we refresh it and then process the request with the refreshed token.
customizeheaders(req: HttpRequest<any>): Observable<HttpRequest<any>> {
// if we have an available token
const request = this.hasSecurity() ?
this.enrichRequest(req) :
req;
// manage expired token
if (this.userMetadata && this.userMetadata.$expiresAt &&
new Date().getTime() + 60000 > this.userMetadata.$expiresAt &&
this.token && this.token.refresh_token) { // we have a 1mn safe delay
const form = new URLSearchParams();
form.set('grant_type', 'refresh_token');
form.set('refresh_token', this.token.refresh_token);
// refresh the token
return this.http.post(environment.meecrogateOauth2Server + '/api/security/login', form.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
}).pipe(
map(token => this.onToken(<Token>token)),
map(() => this.enrichRequest(req)),
catchError(this.errorService.handleError<any>('getToken')));
}
return from([request]);
}
enrichRequest(req: HttpRequest<any>) {
return req.clone({ headers: req.headers.set('Authorization', `${this.token.token_type} ${this.token.access_token}`) });
}
We now have the authentication process working so the user can access protected apis and display those information in our angular application.
Conclusion
In few steps we have been able to secure our web application with the Meecrogate Oauth2 Server. In this example we have been focusing on the PKCE flow but it would be quite simple to adapt the code above for other flows.