NAME

Mojolicious::Plugin::OIDC - OIDC protocol integration for Mojolicious

DESCRIPTION

This plugin makes it easy to integrate the OpenID Connect protocol into a Mojolicious application.

It essentially uses the OIDC-Client distribution.

METHODS

register

Code executed once when the application is loaded.

Depending on the configuration, creates and keeps in memory one or more clients (OIDC::Client stateless objects) and automatically adds the callback routes to the application.

METHODS ADDED TO THE APPLICATION

oidc( $provider )

# with just one provider
my $oidc = $c->oidc;
# or
my $oidc = $c->oidc('my_provider');

# with several providers
my $oidc = $c->oidc('my_provider_1');

Creates and returns an instance of OIDC::Client::Plugin with the data from the current request and session.

If several providers are configured, the $provider parameter is mandatory.

This is the application's entry point to the library. Please see the OIDC::Client::Plugin documentation to find out what methods are available.

CONFIGURATION

Section to be added to your configuration file :

oidc_client => {
  provider => {
    provider_name => {
      id                   => 'my-app-id',
      secret               => 'xxxxxxxxx',
      well_known_url       => 'https://yourprovider.com/oauth2/.well-known/openid-configuration',
      signin_redirect_path => '/oidc/login/callback',
      scope                => 'openid profile roles email',
      expiration_leeway    => 20,
      claim_mapping => {
        login     => 'sub',
        lastname  => 'lastName',
        firstname => 'firstName',
        email     => 'email',
        roles     => 'roles',
      },
      audience_alias => {
        other_app_name => {
          audience => 'other-app-audience',
        }
      }
    }
  }
}

This is an example, see the detailed possibilities in OIDC::Client::Config.

SAMPLES

Here are some samples by category. Although you will have to adapt them to your needs, they should be a good starting point.

Setup

To setup the plugin when the application is launched :

$app->plugin('OIDC');

Authentication

To authenticate the end-user :

$app->hook(before_dispatch => sub {
  my $c = shift;

  my $path = $c->req->url->path;

  # Public routes
  return if $path =~ m[^/oidc/]
         || $path =~ m[^/error/];

  # Authentication
  if (my $identity = $c->oidc->get_stored_identity()) {
    $c->remote_user($identity->{subject});
  }
  elsif (uc($c->req->method) eq 'GET' && !$c->is_ajax_request()) {
    $c->oidc->redirect_to_authorize();
  }
  else {
    $c->render(template => 'error',
               message  => "You have been logged out. Please try again after refreshing the page.",
               status   => 401);
  }
});

API call

To make an API call with propagation of the security context (token exchange) :

# Retrieving a web client (Mojo::UserAgent object)
my $ua = try {
  $c->oidc->build_api_useragent('other_app_name')
}
catch {
  $c->log->warn("Unable to exchange token : $_");
  $c->render(template => 'error',
             message  => "Authorization problem. Please try again after refreshing the page.",
             status   => 403);
  return;
} or return;

# Usual call to the API
my $res = $ua->get($url)->result;

Resource Server

To check an access token from a Resource Server, assuming it's a JWT token. For example, with an application using Mojolicious::Plugin::OpenAPI, you can define a security definition that checks that the access token is intended for all the expected scopes :

$app->plugin(OpenAPI => {
  url      => "data:///swagger.yaml",
  security => {
    oidc => sub {
      my ($c, $definition, $scopes, $cb) = @_;

      my $claims = try {
        return $c->oidc->verify_token();
      }
      catch {
        $c->log->warn("Token validation : $_");
        return;
      } or return $c->$cb("Invalid or incomplete token");

      foreach my $expected_scope (@$scopes) {
        unless ($c->oidc->has_scope($expected_scope)) {
          return $c->$cb("Insufficient scopes");
        }
      }

      return $c->$cb();
    },
  }
});

Another security definition that checks that the user has at least one expected role :

$app->plugin(OpenAPI => {
  url      => "data:///swagger.yaml",
  security => {
    oidc => sub {
      my ($c, $definition, $roles_to_check, $cb) = @_;

      my $user = try {
        $c->oidc->verify_token();
        return $c->oidc->build_user_from_userinfo();
      }
      catch {
        $c->log->warn("Token/User validation : $_");
        return;
      } or return $c->$cb('Unauthorized');

      foreach my $role_to_check (@$roles_to_check) {
        if ($user->has_role($role_to_check)) {
          return $c->$cb();
        }
      }

      return $c->$cb("Insufficient roles");
    },
  }
});

SECURITY RECOMMENDATION

It is highly recommended to configure the framework to store session data, including sensitive tokens such as access and refresh tokens, on the backend rather than in client-side cookies. Although cookies can be signed and encrypted, storing tokens in the client exposes them to potential security threats.

AUTHOR

Sébastien Mourlhou

COPYRIGHT AND LICENSE

Copyright (C) Sébastien Mourlhou

This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.

SEE ALSO