leonardochaia / angular-simple-oidc

Observables all the way, expressive errors, simple API. - Code Flow, Refresh Tokens, Session Management

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

angular-simple-oidc

An Angular library for the Open Id Connect Protocol implementing:

  • Discovery Document
  • Code Flow
  • Refresh Tokens
  • Automatic Token Refresh before token expiring
  • Session Checks
  • Code Flow in popup

Angular Versioning

For Angular 8, use the v8.x latest release.

For Angular 9, use the v9.x latest release.

Using master branch is not recommended.

Disclaimer

I am not a security expert. I've followed my best the RFCs, please I encourage you to review my work and report issues if you suspect something may be wrong.

Use at your own risk.

Motivation

Why another OIDC library? I needed a library that is:

  • Compilant with the OIDC protocol.
  • Returns descriptive error messages.
  • Works well with observables.
  • Integrates seamlesly with NGRX.
  • PRs are merged with caution.
  • OIDC protocol practically does not change, so I expect only bugfixes to be merged once it's stable.
  • Respects Semantic Versioning.
  • Mayor versions to be released together with Angular's.
  • Basic features. More features only if I need them (refresh tokens, session checks) through other modules/packages.

How to use

Installation

Install from NPM

yarn add angular-simple-oidc

Configuring your Identity Provider

Authorization Code Flow with PKCE will be used, so your Identity Provider (idp) must support this. Your idp may require a client secret as well.

The redirect URI, by default will be http://yourapp.example.com/oidc-token-callback. You can change the path by configuring the tokenCallbackRoute.

Static Configuration

You will then need to supply the configuration for OIDC. Configuration can be provided statically for in the app.module file. You could use environment.ts config replacement for different prod/staging/development settings. i.e:

WARNING: The clientSecret should not be treated as a secret. Some IdentityProviders require a secret for Code Flow, some don't.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

import {
  AngularSimpleOidcModule,
  AutomaticRefreshModule,
  SessionManagementModule,
  PopupAuthorizationModule
} from 'angular-simple-oidc';

@NgModule({
  imports: [
    BrowserModule,

    AngularSimpleOidcModule.forRoot({
      clientId: 'example.client',
      // clientSecret: 'myDummySecret',
      openIDProviderUrl: 'http://my.oidc.provider.com',
      scope: 'openid profile offline_access',

      // Other relevant configs.
      // tokenCallbackRoute: 'oidc-token-callback'
    }),

    // For automatic token refresh.
    // AutomaticRefreshModule,

    // For Session Management (read below)
    // SessionManagementModule.forRoot({
    //   iframePath: "assets/oidc-iframe.html"
    // })

    // For popup code flow (read below)
    // PopupAuthorizationModule.forRoot({
    //   childWindowPath: 'assets/oidc-iframe.html'
    // }),

  ],
  declarations: [
    AppComponent,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Asynchronous Configuration

The downside of using static configuration is that your config ends up hard-coded in your app bundle. You can customize the configuration provision by using a service and overriding Angular Simple OIDC configuration providers.

Check the custom-config for a complete example

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, Provider } from '@angular/core';

import { AppComponent } from './app.component';
import { AngularSimpleOidcModule, AUTH_CONFIG, SessionManagementModule, SESSION_MANAGEMENT_CONFIG } from 'angular-simple-oidc';
import { ExternalConfigService } from './external-config.service';
import { HttpClientModule } from '@angular/common/http';
import { map } from 'rxjs/operators';

// needs to be exporter for AOT
export function getExternalAuthConfigFactory(externalConfig: ExternalConfigService) {
  return externalConfig.config$
    .pipe(map(c => c.auth));
}

export const EXTERNAL_AUTH_CONFIG_PROVIDER: Provider = {
  provide: AUTH_CONFIG,
  useFactory: getExternalAuthConfigFactory,
  deps: [ExternalConfigService],
};

export function getExternalSessionConfigFactory(externalConfig: ExternalConfigService) {
  return externalConfig.config$
    .pipe(map(c => c.session));
}

export const EXTERNAL_SESSION_CONFIG_PROVIDER: Provider = {
  provide: SESSION_MANAGEMENT_CONFIG,
  useFactory: getExternalSessionConfigFactory,
  deps: [ExternalConfigService],
};

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,

    // Do not provide a static config here.
    AngularSimpleOidcModule.forRoot(),
    SessionManagementModule.forRoot()
  ],
  providers: [
    EXTERNAL_AUTH_CONFIG_PROVIDER,
    EXTERNAL_SESSION_CONFIG_PROVIDER,
  ],
  declarations: [
    AppComponent,
  ],
  bootstrap: [AppComponent],
})
export class AppModule { }

Requesting more scopes

Currently you can only provide scopes in the configuration. WIP #11

AuthGuard

When using the router, you can use the AuthGuard to prevent a route from loading without a valid Access Token. If the token is not valid or expired, Code Flow will be started.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from 'angular-simple-oidc';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {
    path: '',
    canActivate: [AuthGuard],
    component: HomeComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

AuthService

You can manually start the Code Flow injecting the AuthService and calling startCodeFlow(). This will trigger a redirect to the IDP authorize (aka login) page. You normally want to provide a never-stopping loading on subscription, since the window will be redirected. i.e:

export class MyComponent {
    public loadingForEver = false;
    constructor(private readonly auth: AuthService) { }

    public login() {
        this.auth.startCodeFlow()
        .subscribe(()=>{
            this.loadingForEver = true;
        });
    }
}

When the IDP redirects back to your app, angular-simple-oidc will take care of parsing the callback URL, validating and storing tokens and redirecting back to the state where you were when you started the Code Flow.

You can obtain the tokens by using the AuthService observables. i.e:

export class MyComponent {
    constructor(private readonly auth: AuthService) {
        auth.accessToken$.subscribe(token =>{
            console.info(`Current Access token: ${token}`);
            // This will return the current token (it's a BehaviorSubject)
            // and will emit when a new token is obtained.
            // Note that a token will be returned even if it's expired
        });
        // Remember to takeUntil(this.destroyed$) or use the async pipe to prevent memory leaks.
    }
}

Note that a token will be returned even if it's expired;. To check if the user "is logged in", meaning that has an Access Token and the token is not yet expired you must use the isLoggedIn$ observable instead.

End Session

To log out the user, use the AuthService.endSession(). Will redirect to the log out page of the IdentityProvider.

Automatic refresh

The AutomaticRefreshModule will attempt to refresh the token using Refresh Tokens just before the tokens expire.

Session Management

The SessionManagementModule will use an iframe to an endpoint in the Identity Provider to check if the session is still active. If the session has finished, SessionTerminatedEvent will be fired.

You'll need to provide an HTML file in your /assets directory with the below contents:

<html>
<!-- This file is required for angular-simple-oidc Session Management -->
<body>
    <script>
        (window.opener || window.parent).postMessage(location.href, location.origin);
    </script>
</body>

</html>

Popup code flow

Code flow can be executed in a poup using the PopupAuthorizationModule. This is most useful when the SessionTerminatedEvent has been fired, and the user may log-in using the same credentials without loosing the current app state, since there's no redirect. Be aware that the user may log-in using different credentials, to which you'll have to react accordingly.

To configure the popup, firstinclude the module on your app.module and provide a childWindowPath which can be the same file as the iframeUrl of the SessionManagementModule

You'd then use the AuthorizeEndpointPopupClientService like so:

class MyComponent {
  
  constructor(
    private readonly authorizePopupClient: AuthorizeEndpointPopupClientService,
  ) { }

  public popup() {
    this.authorizePopupClient.startCodeFlowInPopup()
          .subscribe();
  }
}

Check the home.component.ts for a full example

About

Observables all the way, expressive errors, simple API. - Code Flow, Refresh Tokens, Session Management

License:MIT License


Languages

Language:TypeScript 97.5%Language:JavaScript 1.5%Language:HTML 0.9%Language:Shell 0.0%Language:CSS 0.0%Language:SCSS 0.0%