NAME

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

DESCRIPTION

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

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.

SEE ALSO