Integrating OneLogin with an Angular application streamlines the authentication process and enhances security. This blog post delves into setting up the OneLogin environment, crafting essential Angular services, and implementing secure token handling with interceptors. We'll also explore how to authenticate requests on the backend.
Setting Up OneLogin
To begin with, you'll need to set up a OneLogin account and create an application to obtain the client ID and client secret.
- Create a OneLogin Account:
- Go to OneLogin and sign up for an account.
- Create a OneLogin Application:
- Navigate to "Apps" > "Add Apps".
- Search for "OIDC" and select "OpenId Connect".
- Fill in the application details.
- In the "Configuration" tab, set the "Redirect URI" to the URL where your Angular app will handle the authorization code (e.g.,
http://localhost:4200/auth-callback
).
3. Get Client ID and Client Secret:
- Go to the "SSO" tab of your OneLogin application.
- Note down the
Client ID
andClient Secret
.
Creating the AuthService in Angular
First, let's set up the AuthService
in Angular to handle login, token retrieval, and token refresh.
// auth.service.ts
import { Injectable } from '@angular/core';
// add values to env file
import { environment } from '../../../environments/environment';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, lastValueFrom } from 'rxjs';
import { Router } from '@angular/router';
interface TokenResponse {
access_token: string;
expires_in: number;
id_token: string;
token_type: string;
refresh_token: string;
}
@Injectable({
providedIn: 'root',
})
export class AuthService {
private clientId = environment.oidc_clientId;
private clientSecret = environment.oidc_clientSecret;
private tokenEndpoint = environment.tokenEndpoint;
private redirectUri = environment.oidc_redirectUri;
private userinfoEndpoint = environment.userinfoEndpoint;
private subdomain = environment.subdomain;
constructor(private http: HttpClient, private router: Router) {}
login(): void {
const loginUrl = `https://${this.subdomain}.onelogin.com/oidc/2/auth?client_id=${this.clientId}&redirect_uri=${this.redirectUri}&response_type=code&scope=openid`;
window.location.href = loginUrl;
}
getToken(authorizationCode: string): Observable<TokenResponse> {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
});
const body = new URLSearchParams();
body.set('client_id', this.clientId);
body.set('client_secret', this.clientSecret);
body.set('grant_type', 'authorization_code');
body.set('code', authorizationCode);
body.set('redirect_uri', this.redirectUri);
return this.http.post<TokenResponse>(this.tokenEndpoint, body.toString(), { headers });
}
generateRefreshToken(refreshToken: string): Observable<TokenResponse> {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
});
const body = new URLSearchParams();
body.set('client_id', this.clientId);
body.set('client_secret', this.clientSecret);
body.set('refresh_token', refreshToken);
body.set('grant_type', 'refresh_token');
return this.http.post<TokenResponse>(this.tokenEndpoint, body.toString(), { headers });
}
getUserInfo(accessToken: string): Observable<any> {
const headers = new HttpHeaders({
Authorization: `Bearer ${accessToken}`,
});
return this.http.get(this.userinfoEndpoint, { headers });
}
async handleAuthorizationCode(authorizationCode: string): Promise<boolean> {
try {
const tokenResponse = await lastValueFrom(this.getToken(authorizationCode));
if (tokenResponse.access_token) {
sessionStorage.setItem('access_token', tokenResponse.access_token);
await this.fetchUserInfo(tokenResponse.access_token);
await this.handleRefreshToken(tokenResponse.refresh_token);
return true;
}
return false;
} catch (error) {
console.error('Error handling authorization code:', error);
return false;
}
}
async fetchUserInfo(accessToken: string): Promise<void> {
try {
const userInfo = await lastValueFrom(this.getUserInfo(accessToken));
// Store user info if needed
} catch (error) {
console.error('Error fetching user info:', error);
}
}
async handleRefreshToken(refreshToken: string): Promise<void> {
try {
const refreshTokenResponse = await lastValueFrom(this.generateRefreshToken(refreshToken));
sessionStorage.setItem('access_token', refreshTokenResponse.access_token);
sessionStorage.setItem('refresh_token', refreshTokenResponse.refresh_token);
console.log('Token refreshed successfully.');
} catch (error) {
console.error('Error refreshing token:', error);
this.router.navigate(['/auth']);
}
}
}
Handling Token Refresh
To ensure tokens are refreshed periodically, we need to set up a TokenRefreshService
.
// token-refresh.service.ts
import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class TokenRefreshService {
private scheduleTokenTimer: number = 120000; // 120,000 milliseconds = 2 minutes
constructor(private authService: AuthService, private router: Router) {
this.scheduleTokenRefresh();
}
private async handleRefreshToken(): Promise<void> {
const refreshToken = sessionStorage.getItem('refresh_token');
if (refreshToken) {
try {
await this.authService.handleRefreshToken(refreshToken);
} catch (error) {
console.error('Error refreshing token:', error);
this.router.navigate(['/auth']);
}
} else {
console.log('No refresh token found in session storage.');
}
}
private scheduleTokenRefresh(): void {
setInterval(() => {
this.handleRefreshToken();
}, this.scheduleTokenTimer);
}
}
Adding an Interceptor to Angular
To ensure all HTTP requests include the access token, we need to add an HTTP interceptor.
// token.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { Router } from '@angular/router';
export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const authToken = sessionStorage.getItem('access_token');
if (authToken) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${authToken}`,
},
});
}
return next(req).pipe(
catchError((error) => {
if (error.status === 401) {
router.navigate(['/auth']);
}
return throwError(error);
})
);
};
Configuring Angular to Use the Interceptor
// app.config.ts
import { ApplicationConfig, APP_INITIALIZER } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { tokenInterceptor } from './token.interceptor';
import { TokenRefreshService } from './token-refresh.service';
export function initializeApp(tokenRefreshService: TokenRefreshService) {
return (): void => {
tokenRefreshService.handleRefreshToken();
};
}
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideAnimationsAsync(),
provideHttpClient(
withInterceptors([tokenInterceptor])
),
TokenRefreshService,
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
deps: [TokenRefreshService],
multi: true,
},
],
};
Note: Ensure that the application is set to POST, not BASIC
Authenticating in the Backend
To authenticate in the backend, you can follow the OneLogin developer documentation on how to verify tokens:
OneLogin Backend Authentication Documentation
This documentation provides detailed steps on how to validate the JWT tokens received from OneLogin.
Conclusion
By following these steps, you can securely integrate OneLogin with your Angular application. The AuthService
handles login and token retrieval, while the TokenRefreshService
ensures tokens are refreshed periodically. Adding an HTTP interceptor ensures all requests include the access token, maintaining secure communication with your backend.