OAuth 2.0 and OpenID Connect Implementation in Laravel (Authlete)

Good news for Laravel users!

authlete-laravel library was released. With this library, you can implement a full-fledged authorization server and OpenID provider which utilizes an OpenID-certified authorization engine, Authlete.

This article shows how to implement an authorization server and a resource server which support OAuth 2.0 and OpenID Connect in Laravel. For the implementation, authlete/authlete library (for PHP) and authlete/authlete-laravel library (for Laravel) are used.

Laravel is used as a real example, but this article also explains common basics for implementing the servers as necessary. Therefore, you can start reading this article without prior deep knowledge.

0. For the impatient

$ laravel new authorization-server
$ cd authorization-server
$ composer require authlete/authlete-laravel
$ vi config/app.php
# Add the following manually to 'providers' if the version of
# Laravel is older than 5.5.
# Authlete\Laravel\Provider\AuthleteServiceProvider::class
$ php artisan authlete:authorization-server
$ vi config/authlete.php
# Set your API key and API secret issued by Authlete.
$ vi .env
# Change the value of DB_CONNECTION to 'sqlite'.
# Comment out other variables that start with 'DB_'.
$ touch database/database.sqlite
$ php artisan make:auth
$ php artisan migrate
$ php artisan serve
# Register a user at http://127.0.0.1:8000/register

Access token request by the implicit flow:

http://127.0.0.1:8000/authorization?response_type=token&client_id={CLIENT-ID}

ID token request by the implicit flow:

http://127.0.0.1:8000/authorization?response_type=id_token&client_id={CLIENT-ID}&redirect_uri=https://api.authlete.com/api/mock/redirection/{SERVICE-API-KEY}&scope=openid+email+profile&nonce={NONCE}

Authorization request in the authorization code flow:

http://127.0.0.1:8000/authorization?response_type=code&client_id={CLIENT-ID}

Token request in the authorization code flow:

$ curl http://127.0.0.1:8000/api/token \
-d grant_type=authorization_code \
-d code={AUTHORIZATION-CODE} \
-d client_id={CLIENT-ID}

1. What to do to publish APIs?

If you have reached a conclusion that your service should publish Web APIs as Facebook does, what you have to develop are the following two servers.

  • Authorization Server, whose job is to issue access tokens.
  • Resource Server, whose job is to publish Web APIs.

The concept of an authorization server and a resource server is explained in The Simplest Guide To OAuth 2.0. However, as the explanation is for beginners, only with the information written in the article, it is difficult to imagine how the servers are implemented. Therefore, this article digs a bit technically deeper.

What an authorization server needs to provide are the following two endpoints.

If you limit use cases, either of them may be enough. However, to support the flow called “authorization code flow” (RFC 6749, 4.1) which is used most often, both the endpoints need to be provided. Regarding the relationship between flows and endpoints, please read Diagrams And Movies Of All The OAuth 2.0 Flows and Diagrams Of All The OpenID Connect Flows.

A client application obtains an access token by communicating with either or both of an authorization endpoint and a token endpoint.

Image for post
Image for post

It is RFC 6749 (The OAuth 2.0 Authorization Framework) that defines details about the communication between a client application and the endpoints. Therefore, engineers read RFC 6749 to develop an authorization server. In addition, they refer to other specifications such as RFC 7636 (Proof Key for Code Exchange by OAuth Public Clients) and “OpenID Connect Core 1.0 as necessary.

An authorization may provide other endpoints than an authorization endpoint and a token endpoint. For example, there exist endpoints like below.

  • Introspection Endpoint (RFC 7662) — provides information about access tokens.
  • Revocation Endpoint (RFC 7009) — revokes access tokens.
  • Discovery Endpoint (OIDC Discovery 1.0) — provides information about the configuration of the server.
  • JWK Set Document Endpoint (RFC 7517) — provides the JWK Set Document of the server.

A resource server provides Web APIs.

It is okay to define and publish any Web API as you like. RFC 6749 does not discuss what kind of Web APIs should be defined and published, but instead, the specification (and RFC 6750) describes how to protect Web APIs. The basic idea is as follows.

  1. Issue an access token which represents the right to access the Web API.
  2. Give the access token to the client application in advance.
  3. The client application brings the access token when it accesses the Web API.
  4. The Web API returns an expected response if the presented access token is valid, or returns an error response if the access token is invalid.
Image for post
Image for post

It is possible to devise as many ways for a Web API to receive an access token as you like. However, if there is no special reason, it is wise to follow the standard ways defined in RFC 6750 (The OAuth 2.0 Authorization Framework: Bearer Token Usage).

2. Authlete

In this article, an authorization server and a resource server are implemented with authlete/authlete-laravel library. This library, to be exact, authlete/authlete library which is internally used by authlete/authlete-laravel, communicates with a service called “Authlete”.

Authlete is a service which provides functions necessary to implement OAuth 2.0 and OpenID Connect as Web APIs. Authorization servers and resource servers which use Authlete communicate with Authlete from within their implementations.

Image for post
Image for post

The logic of OAuth 2.0 and OpenID Connect is implemented in Authlete, and access tokens and other data that should be stored in a persistent storage are managed in the database on Authlete side. Therefore, using Authlete makes it much easy to implement an authorization server.

Regarding details about the architecture of Authlete, please read this article:

A pair of API key and API secret issued by Authlete is necessary to use Authlete APIs. If you don’t have any yet, please get one with the instruction written in “3.1. API key and API secret to use Authlete APIs” of Spring + OAuth 2.0 + OpenID Connect.

A client ID is necessary when you make requests to an authorization server. As you’ll need one later, please get one with the instruction written in “3.2. Client ID” of Spring + OAuth 2.0 + OpenID Connect.

(This section can be skipped as it is not relevant to the subsequent technical sections.)

Authlete, Inc. is a company that was incorporated in September, 2015. As of May 2018, the company has offices in Level39 (Canary Wharf, London) and in FINOLAB (Otemachi, Tokyo).

Its main service is Authlete (the same name as the company name). In the next year of the incorporation, Authlete was adopted as the authorization server for the big healthcare service having about 10 million users operated by MTI (TYO:9438). After Authlete won the grand prize at FIBC (Financial Innovation Business Conference) in 2017 (presentation), adoption of Authlete has continued to grow, for example, as the authorization engine for Uni-ID Libra, a CIAM (Consumer Identity and Access Management) package solution by NRI Secure Technologies, and also for Resonatex, an Open API platform based on the ledger system by Nihon Unisys. Some banks, such as Seven Bank that built its bank API using Microsoft Azure and Authlete, have begun adopting Authlete, too.

In May 2017, Authlete, Inc. announced the closing of its US$1.2 million seed round after attracting funds from MTI (TYO:9438) and 500 Startups Japan. And in February 2018, Toppan Printing (TYO:7911), NTT DOCOMO Ventures and MTI joined the pre-series A round raising US$3m.

3. Authorization server implementation

Let’s implement an authorization server.

$ laravel new authorization-server
$ cd authorization-server
$ composer require authlete/authlete-laravel

If the version of your Laravel is older than 5.5, automatic package discovery does not work. Therefore, you need to add AuthleteServiceProvider to the list of providers manually. Open config/app.php and add it to providers as follows.

'providers' => [
// Other service providers
Authlete\Laravel\Provider\AuthleteServiceProvider::class,
],

Run authlete:authorization-server command, which is an Artisan command added by authlete-laravel.

$ php artisan authlete:authorization-server

The command conducts the following.

  • Generates the configuration file config/authlete.php (but won’t overwrite the file if it already exists).
  • Adds routes to routes/web.php.
  • Adds routes to routes/api.php.
  • Generates controllers under app/Http/Controllers/Authlete/.
  • Generates authorization.blade.php under resource/views/authlete/.
  • Generates authorization.css under public/css/authlete/.

Regarding details about the behavior of authlete:authorization-server, please refer to AuthleteAuthorizationServerCommand.php.

A configuration file for Authlete, config/authlete.php, is generated by authlete:authorization-server. Its initial content is as follows.

<?php
return [
'base_url' => 'https://api.authlete.com',
'service_owner_api_key' => '',
'service_owner_api_secret' => '',
'service_api_key' => '',
'service_api_secret' => ''
];
?>

The values of service_api_key and service_api_secret are empty. Please fill them in with the API key and the API secret issued by Authlete.

Before getting down to an authorization endpoint implementation, let’s implement a discovery endpoint in order to check whether connection to Authlete can be established.

Discover endpoint is an endpoint that returns information about server configuration in JSON format. Details are defined in 4. Obtaining OpenID Provider Configuration Information of OpenID Connect Discovery 1.0. An example response is listed in 4.2. OpenID Provider Configuration Response of the specification. A real-world example is https://accounts.google.com/.well-known/openid-configuration by Google.

authlete-laravel contains an implementation of discovery endpoint, DefaultConfigurationController class. The class is a complete implementation, so additional work is not required.

ConfigurationController class which extends DefaultConfigurationController is generated under app/Http/Controllers/Authlete/ folder by authlete:authorization-server, so ConfigurationController can be used as an implementation of discovery endpoint without modification.

Open routes/web.app and add a route for the discovery endpoint as follows.

Route::get('/.well-known/openid-configuration',
'\App\Http\Controllers\Authlete\ConfigurationController');

… well, actually, you don’t have to do anything here because the content above has already been written in routes/web.php by authlete:authorization-server.

Start the built-in web server by php artisan serve.

$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

The web server listens to the port 8000. Access the discovery endpoint by a Web browser or curl command.

http://127.0.0.1:8000/.well-known/openid-configuration

A successful response is JSON like below.

Image for post
Image for post

The path of the discovery endpoint is /.well-known/openid-configuration. Regarding this path, please read the description in 4. Obtaining OpenID Provider Configuration Information of OpenID Connect Discovery 1.0.

The job of an authorization server is to issue access tokens to client applications. An access token represents that a user has granted permissions to a client application.

When an authorization server receives a request for an access token from a client application, the server asks the user whether to grant permissions to the client application before issuing an access token. For that purpose, the server shows an authorization page which has the structure like below.

Image for post
Image for post

An authorization server implementation has to generate an authorization page like this and show it to the user.

The login form in the authorization page shown in the previous section exists for user authentication.

In the example above, users are authenticated by a login ID and a password. However, any means can be adopted as long as it can identify and authenticate the user. For example, fingerprint, iris recognition, hardware token, etc. can be used. In addition, two or more means can be used combinedly for better authenticity. It’s known as “multi-factor authentication”.

The specification of OAuth 2.0 is, as its name (The OAuth 2.0 Authorization Framework) indicates, for authorization and not for authentication. 3.1. Authorization Endpoint of RFC 6749 explicitly states that user authentication is “beyond the scope of this specification”.

The authorization endpoint is used to interact with the resource owner and obtain an authorization grant. The authorization server MUST first verify the identity of the resource owner. The way in which the authorization server authenticates the resource owner (e.g., username and password login, session cookies) is beyond the scope of this specification.

However, because user authentication is included as one step during the authorization process, an authorization server has to implement a mechanism for user authentication.

Implementing a mechanism for user authentication is off topic from the subject of this article. So, let’s use “Laravel Authentication”.

First, let’s configure the database which stores user information. MySQL is the default database, but here we use SQLite for simplicity.

Open .env file, and you will find some entries that start with DB_.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

Change the value of DB_CONNECTION from mysql to sqlite, and comment out other entries that start with DB_.

DB_CONNECTION=sqlite
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=homestead
#DB_USERNAME=homestead
#DB_PASSWORD=secret

Then, create an empty database file.

$ touch database/database.sqlite

Run make:auth command to set up routes and views for user authentication.

$ php artisan make:auth
Authentication scaffolding generated successfully.

Run migrate command to create database tables.

$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table

Restart the built-in web server (php artisan serve) and register a user account at the registration page (http://127.0.0.1:8000/register).

Image for post
Image for post

An authorization endpoint does not send an access token back immediately after it receives an authorization request from a client application. Before issuing an access token, an authorization endpoint confirms with the user whether to approve the request. For the confirmation, the authorization endpoint sends back an authorization page (HTML) instead of an authorization code or an access token.

The user checks the content of the authorization request displayed in the authorization page, decides to approve or reject the authorization request, and finally press either the “approve” button or the “reject” button. The decision is submitted to the authorization server. It is after receiving the decision that the authorization endpoint issues an authorization code and/or an access token.

The following diagram illustrates the steps described above.

Image for post
Image for post

To tell the truth, as the diagram shows, an authorization server provides at least two endpoints to process an authorization request. However, the specification does not mention it explicitly and conceptually calls the collection of the endpoints “authorization endpoint”.

Highlighted parts in the diagram below are the ones mentioned in RFC 6749.

Image for post
Image for post

Among the flows defined in RFC 6749, the following two use an authorization endpoint.

HTTP requests that client applications are expected to send in these flows are described in 4.1.1. Authorization Request and 4.2.1. Authorization Request, respectively.

Both of the flows have the same request parameters as listed below.

  • response_type — tokens that the authorization endpoint should return
  • client_id — the identifier of the client application
  • redirect_uri — the destination to which the final response should be sent
  • scope — the permissions the client application is asking for
  • state — an arbitrary state parameter

The response_type parameter represents what the client application wants the authorization endpoint to return. If its value is code, an authorization code is requested. If its value is token, an access token is requested. From a different point of view, the response_type parameter indicates which flow is used. If its value is code, the authorization code flow is used. If its value is token, the implicit flow is used.

However, the old good days in which we could determine the flow by checking whether the value of the response_type parameter was either code or token have ended due to the release of OpenID Connect. Now, the response_type parameter can be “any combination of code, token and id_token” or “none”. Details are defined in OAuth 2.0 Multiple Response Type Encoding Practices.

In addition to introducing the complexity to the response_type parameter, OpenID Connect has added new request parameters as listed below.

  • response_mode
  • nonce
  • display
  • prompt
  • max_age
  • ui_locales
  • id_token_hint
  • login_hint
  • acr_values
  • claims_locales
  • claims
  • request
  • request_uri
  • registration

It is not only OpenID Connect but also other specifications that have added new parameters. For example, RFC 7636 (Proof Key for Code Exchange by OAuth Public Clients) has added the following request parameters.

  • code_challenge
  • code_challenge_method

We can manage to read RFC 6749, but OpenID Connect Core 1.0 is so long that it is difficult to read it through without strong willpower. In addition, knowledge about the following specifications is required as prerequisites. Furthermore, there are other related specifications.

The volume of the specifications is so huge and one bug in the implementation may cause a security vulnerability directly, so it is a tough task to implement an authorization server and OpenID provider from scratch. It is the place Authlete comes in.

The authorization endpoint of an authorization server which uses Authlete as its backend system passes request parameters it received from a client application to Authlete’s /api/auth/authorization API without interpreting them. Interpretation of the request parameters is conducted by the implementation of /api/auth/authorization API.

A response from /api/auth/authorization API contains the following.

  • Information about the authorization request
  • The next action that the authorization endpoint should take
  • A response that should be returned to the client application (in case the authorization request is wrong)

For example, if the client_id request parameter is not included in an authorization request, /api/auth/authorization API returns a response like “Return ‘400 Bad Request’ to the client application. Use ‘…’ as the error message of the response.” If an authorization request is valid, it returns a response like “The authorization request has no problem. Get the user’s consent to the authorization request interactively. Then, call either /api/auth/authorization/issue API or /api/auth/authorization/fail API according to the user’s decision.”

The snippet below shows the process right after an authorization endpoint receives an authorization request.

public function __invoke(AuthleteApi $api, Request $request)
{
// Call Authlete's /api/auth/authorization API.
$response = $this->callAuthorizationApi($api, $request);
// 'action' in the response denotes the next action which
// this authorization endpoint implementation should take.
$action = $response->getAction();
// Dispatch according to the action.
switch ($action)
{
case AuthorizationAction::$INTERACTION:
// Process the authorization request with user
// interaction. An authorization page is returned.
return $this->handleInteraction(
$api, $request, $response);
case AuthorizationAction::$NO_INTERACTION:
// Process the authorization request without user
// interaction. The flow reaches here only when
// the authorization request contains 'prompt=none'.
return $this->handleNoInteraction(
$api, $request, $response);
default:
// Handle error cases here.
return $this->handleError(
$api, $request, $response);
}
}

An implementation of authorization endpoint is already ready thanks to authlete:authorization-servercommand. The following files are starting points, so please read the implementations as necessary.

  • app/Http/Controllers/Authlete/AuthorizationController
  • app/Http/Controllers/Authlete/AuthorizationDecisionController
  • resources/views/authlete/authorization.blade.php
  • public/css/authlete/authorization.css

For customizability, the implementation is layered and componentized. Hints to read the implementations are as follows.

  • Controllers under app/Http/Controllers/Authlete extend controllers under \App\Laravel\Controller.
  • Therefore, a simple behavior change can be achieved by overriding methods in controllers under app/Http/Controllers/Authlete.
  • Some overridable methods return an implementation of ???Spi interface. Definitions of SPIs (Service Provider Interfaces) exist in \Authlete\Laravel\Handler\Spi. SPIs are big customization points.
  • The default implementations of the SPIs exist in \Authlete\Laravel\Handler\Spi. They use Laravel Authentication. You need to implement SPIs if your system does not use Laravel Authentication.
  • SPIs are used to adjust behaviors of handlers. Handlers exist in \Authlete\Laravel\Handler.
  • It is okay to call Authlete APIs directly without using the handlers. AuthleteApi interface and its implementation AuthleteApiImpl are defined in authlete/authlete library. All the Authlete API calls are gathered in the AuthleteApi interface. The authlete/authlete library can be used also in other PHP frameworks than Laravel.

OpenID Connect has the concept of “maximum authentication age”. Also, it has defined the auth_time claim which represents the time at which the user was authenticated. For these, an OpenID provider implementation has to know user authentication time. However, the initial and minimum Laravel Authentication set up by php artisan make:auth does not manage user authentication time.

It is possible to modify Laravel Authentication to record user authentication time. There are blog articles explaining how to achieve it. So, it is technically possible to modify the implementation of authlete:authorization-server command to add such an extension to Laravel Authentication. However, if such a behavior were added to authlete:authorization-server, the command would depend on Laravel Authentication too tightly.

Therefore, authlete:authorization-server command has not implemented the mechanism to get user authentication time but instead a customization point is provided. It is getUserAuthenticatedAt() method of AuthorizationController and AuthorizationDecisionController. The initial implementation of the method is as follows.

protected function getUserAuthenticatedAt(
User $user, Request $request)
{
return parent::getUserAuthenticatedAt($user, $request);
}

If you change the implementation of this method so that it can return user authentication time in seconds since the Unix epoch (1970-Jan-1), your authorization server can support maximum authentication age and the auth_time claim.

Now we have implemented an authorization endpoint, let’s make an authorization request.

Let’s make an access token request by the implicit flow which uses the authorization endpoint only. According to 4.2.1. Authorization Request in RFC 6749, mandatory request parameters are response_type and client_id. As we are going to use the implicit flow, the value of response_type is token. The value of client_id is a client ID issued by Authlete. As a result, an authorization request looks like below.

http://127.0.0.1:8000/authorization?response_type=token&client_id={Client-ID}

Input the URL to the address bar of your web browser, and an authorization page will be displayed.

Image for post
Image for post

For the Login ID field and the Password field, use the email address and the password of the user you registered in “3.8.4. User registration”.

If the following conditions are satisfied, the login form is not displayed.

  • The user has already logged in.
  • The maximum authentication age has not been reached. (The max_age parameter of the authorization request and the default_max_age metadata of the client application affect this.)
  • The prompt parameter of the authorization request does not contain login.

Press the “Authorize” button to submit the form, and the authorization server will return an HTTP response with the status code “302 Found”.

HTTP/1.1 302 Found
Location: {REDIRECT-URI}?#access_token={ACCESS-TOKEN}&...
Cache-Control: no-store
Pragma: no-cache

If a web browser receives this response, it will open the URL written in the Location header.

The URL written in the Location header is one of the URLs that the client application has registered with the authorization server in advance. The URL is called “redirect URI”. If you have not changed the value of the redirect URIs since you created a client application, the value is https://api.authlete.com/api/mock/redirection/{SERVICE-API-KEY}. Therefore, the web browser will access /api/mock/redirection/{SERVICE-API-KEY} on the Authlete server.

The implementation of /api/mock/redirection/{SERVICE-API-KEY} displays the content of the request it receives.

Image for post
Image for post

The value next to access_token that starts with pQ45 is the access token issued by the authorization server.

Since OpenID Connect is supported, let’s make an ID token request.

To request an authorization endpoint to issue an ID token, include id_token in the response_type parameter. The specification of the request in this case is written in 3.2.2.1. Authentication Request of OpenID Connect Core 1.0.

Reading the specification will reveal that there are various mandatory parameters.

  • response_type — tokens that the authorization endpoint should return. id_token needs to be included.
  • client_id — the identifier of the client application
  • redirect_uri — the destination to which the final response should be sent
  • scope — the permissions the client application is asking for. openid must be included.
  • noncenonce

In RFC 6749, the redirect_uri parameter can be omitted under some conditions, but it is mandatory in OpenID Connect.

OpenID Connect requests must include openid in the scope parameter. OpenID Connect has defined some other standard scope names. Among them, profile, email, address and phone control claims that are embedded in an ID token. Details are defined in 5.4. Requesting Claims using Scope Values of OpenID Connect Core 1.0. The example request that will be shown soon later includes not only openid but also profile and email in the scope parameter.

Even in OpenID Connect, nonce parameter is optional in the authorization code flow (3.1.2.1. Authentication Request). However, it is mandatory in the implicit flow (3.2.2.1. Authentication Request).

In summary, an ID token request by the implicit flow looks like below.

http://127.0.0.1:8000/authorization?response_type=id_token&client_id={CLIENT-ID}&redirect_uri=https://api.authlete.com/api/mock/redirection/{SERVICE-API-KEY}&scope=openid+email+profile&nonce={NONCE}

This request will show an authorization page like below.

Image for post
Image for post

Input the login ID & the password (if the login form is displayed) and press the “Authorize” button, and an ID token will be issued.

Image for post
Image for post

A token endpoint is used by other flows than the implicit flow (RFC 6749, 4.2). To be exact, the following flows use a token endpoint.

All the flows have the grant_type parameter. The value of the parameter determines which flow is used. For example, the value of the grant_type parameter is authorization_code for the authorization code flow. password is used for the resource owner password credentials flow.

Mandatory parameters vary depending on the flow. For example, in the authorization code flow, the code parameter is mandatory (4.1.3. Access Token Request). In the resource owner password credentials flow, the username parameter and the password parameter are mandatory (4.3.2. Access Token Request).

In addition, to support RFC 7636, a token endpoint must be able to recognize the code_verifier parameter.

Likewise the implementation of authorization endpoint, the implementation of token endpoint passes token request parameters it received from a client application to Authlete’s /api/auth/token API without interpreting them. The implementation of /api/auth/token API interprets the token request parameters and issues an access token and optionally an ID token.

The resource owner password credentials flow is only an exception. A call of /api/auth/token API does not complete the flow. The implementation of token endpoint has to call either /api/auth/token/issue API or /api/auth/token/fail API additionally. It’s because Authlete has to give the control back to the implementation of token endpoint temporarily before issuing tokens in order to delegate user authentication to the authorization server.

The process right after a token endpoint receives a token request will look like the following.

public function handle(Request $request)
{
// Call Authlete's /api/auth/token API.
$response = $this->callTokenApi($request);
// 'action' in the response denotes the next action which
// the implementation of token endpoint should take.
$action = $response->getAction();
// The content of the response to the client application.
$content = $response->getResponseContent();
// Dispatch according to the action.
switch ($action)
{
case TokenAction::$INVALID_CLIENT:
// 401 Unauthorized
return ResponseUtility::unauthorized(
self::$CHALLENGE, $content);
case TokenAction::$INTERNAL_SERVER_ERROR:
// 500 Internal Server Error
return ResponseUtility::internalServerError($content);
case TokenAction::$BAD_REQUEST:
// 400 Bad Request
return ResponseUtility::badRequest($content);
case TokenAction::$PASSWORD:
// Process the token request whose flow is
// "Resource Owner Password Credentials".
return $this->handlePassword($response);
case TokenAction::$OK:
// 200 OK
return ResponseUtility::okJson($content);
default:
// 500 Internal Server Error.
// This should never happen.
return $this->unknownAction('/api/auth/token');
}
}

An implementation of token endpoint is already ready thanks to authlete:authorization-server command. The following file is the starting point, so please read the implementation as necessary.

  • app/Http/Controllers/Authlete/TokenController

We have implemented both an authorization endpoint and a token endpoint. Now, we can try the main flow, the authorization code flow.

In the authorization code flow, as the first step, an authorization code is issued from an authorization endpoint. A client application presents the authorization code at a token endpoint and receives an access token in exchange. As the lifetime of an authorization code is short (the specification says “A maximum authorization code lifetime of 10 minutes is RECOMMENDED”), a client application must make a token request soon after it receives an authorization code.

The diagram below illustrates the authorization code flow.

Image for post
Image for post

Highlighted parts in the diagram below are the ones mentioned in RFC 6749.

Image for post
Image for post

In the authorization code flow, the value of response_type parameter of an authorization request is code.

http://127.0.0.1:8000/authorization?response_type=code&client_id={CLIENT-ID}

As the same as the examples so far, your browser will be redirected to the redirection endpoint after you press the “Authorize” button. The value of code in the displayed page is the authorization code issued as a result of the authorization request.

Image for post
Image for post

In the authorization code flow, the value of the grant_type parameter of a token request is authorization_code. The value of the mandatory code parameter is an authorization code issued from an authorization endpoint. In addition, if client authentication is not performed, the client_id parameter should be added, too.

As a token endpoint expects POST method (not GET method), the address bar of a web browser cannot send a token request. So, use other means such as curl command to make a token request using POST method.

In summary, a token request in the authorization code flow will look like the following.

$ curl http://127.0.0.1/api/token \
-d grant_type=authorization_code \
-d code={AUTHORIZATION-CODE} \
-d client_id={CLIENT-ID}

A successful response will look like the following.

{
"access_token":"0l1vrjWLDM_hzBXRy2Ydz3w4ij6G893Hjqr5W8_rBEA",
"refresh_token":"lH9Vi6Lpzs6pz7IJq2xm0de--Bf-Rifi3vWmjgVbVdA",
"scope":null,
"token_type":"Bearer",
"expires_in":86400
}

The value of access_token in the JSON is the access token issued as a result of the token request.

By trying the authorization code flow, we used both the authorization endpoint and the token endpoint. This means we could confirm the basic behaviors of the authorization server. 👏👏👏

4. Resource server implementation

$ laravel new resource-server
$ cd resource-server
$ composer require authlete/authlete-laravel

Like in the case of the authorization server, you need to add AuthleteServerProvider to config/app.php manually if the version of your Laravel is older than 5.5.

Run authlete:resource-server command, which is an Artisan command added by authlete-laravel.

$ php artisan authlete:resource-server

The command conducts the following.

  • Generates the configuration file config/authlete.php (but won’t overwrite the file if it already exists).
  • Adds routes to routes/api.php.
  • Generates UserInfoController under app/Http/Controllers/Authlete/.

Regarding details about the behavior of authlete:resource-server, please refer to AuthleteResourceServerCommand.php.

A configuration file for Authlete, config/authlete.php, is generated by authlete:resource-server. But, if the file already exists, it won’t be overwritten. Set the API key and the API secret issued by Authlete.

Let’s try to implement an API and protect it by access tokens.

As the first step, let’s create an API that returns information about the current time in JSON format. Create a new file, app/Http/Controllers/TimeController.php, and write the content shown below into it.

<?php
namespace App\Http\Controllers;

class TimeController extends Controller
{
public function __invoke()
{
return response()->json(getdate());
}
}
?>

Next, open routes/api.php and map TimeController to /api/time. Don’t prepend /api to the path.

Route::get('/time', '\App\Http\Controllers\TimeController');

Let’s check the behavior. Start the resource server by php artisan serve. In the example below, the port number is explicitly specified by the --port option in order to avoid conflict with the authorization server.

$ php artisan serve --port=8001

Access the /api/time API.

$ curl http://127.0.0.1:8001/api/time

A successful response will look like the following.

{
"seconds":20,
"minutes":14,
"hours":15,
"mday":22,
"wday":2,
"mon":5,
"year":2018,
"yday":141,
"weekday":"Tuesday",
"month":"May",
"0":1527002060
}

It is easy to extract an access token in the ways defined in RFC 6750 if an instance of \Illuminate\Http\Request is available.

// RFC 6750, 2.1. Authorization Request Header Field
// Extract an access token from the Authorization header.
$accessToken = $request->bearerToken();

// RFC 6750, 2.2. Form-Encoded Body Parameter
// Extract an access token from the form parameter.
$accessToken = $request->input('access_token');

// RFC 6750, 2.3. URI Query Parameter
// Extract an access token from the query parameter.
$accessToken = $request->query('access_token');

WebUtility class in authlete-laravel library provides extractAccessTokenFrom*() methods that do the same as above.

// RFC 6750, 2.1. Authorization Request Header Field
// Extract an access token from the Authorization header.
$accessToken = WebUtility::extractAccessTokenFromHeader($request);

// RFC 6750, 2.2. Form-Encoded Body Parameter
// Extract an access token from the form parameter.
$accessToken = WebUtility::extractAccessTokenFromBody($request);

// RFC 6750, 2.3. URI Query Parameter
// Extract an access token from the query parameter.
$accessToken = WebUtility::extractAccessTokenFromQuery($request);

In addition, the class provides extractAccessToken() method that tries the three methods above in the order until an access token is found.

// Try the three ways defined in the RFC 6750
// until an access token is found.
$accessToken = WebUtility::extractAccessToken($request);

authlete-laravel library provides AccessTokenValidator class for access token validation.

An access token can be validated by doing:

  1. Create an instance by passing an implementation of the AuthleteApi interface to the constructor,
  2. Set an access token to be validated by setAccessToken() method,
  3. Set conditions for validation by setRequiredScopes() methods and other methods, and
  4. Call validate() method.

The snippet below is confirming that the access token covers at least email scope and profile scope.

// The access token to be validated.
$accessToken = ...;
// An instance that implements the AuthleteApi interface
$api = ...;
// Create an instance of AccessTokenValidator
$validator = new AccessTokenValidator($api);
// Set the access token.
$validator->setAccessToken($accessToken);
// Scopes required to access the API.
$requiredScopes = array('email', 'profile');
$validator->setRequiredScopes($requiredScopes);
// Validate the access token.
$valid = $validator->validate();

The implementation of validate() method internally calls Authlete’s /api/auth/introspection API. After the call of validate() method, if the internal API call has succeeded, the response from /api/auth/introspection API can be obtained by getIntrospectionResponse() method.

// The response from /api/auth/introspection API.
// An instance of \Authlete\Dto\IntrospectionResponse class.
$introspectionResponse = $validator->getIntrospectionResponse();

getIntrospectionResponse() method returns an instance of IntrospectionResponse class. Information about the access token such as the client ID and the user identifier can be obtained from the instance.

// The ID of the client application to which
// the access token has been issued.
$clientId = $introspectionResponse->getClientId();
// The ID of the user who has granted authorization
// to the client application.
$subject = $introspectionResponse->getSubject();
// Scopes covered by the access token.
$scopes = $introspectionResponse->getScopes();

If the access token is invalid or the API call to /api/auth/introspection has failed, getErrorResponse() method returns an error response that should be returned to the client application.

// An error response to the client application.
// An instance of \Illuminate\Http\Response class.
$errorResponse = $validator->getErrorResponse();

Since the type of the instance returned from getErrorResponse() method is \Illuminate\Http\Response, the instance can be used as a response from the controller.

// If the access token is not valid.
if (!$validator->validate())
{
// Return an error response that conforms to RFC 6750.
return $validator->getErrorResponse();
}

AccessTokenValidator class provides create() static method which creates an instance of AccessTokenValidator class in an easier way.

/**
* Create a validator.
*
* The access token is extracted from the `Authorization` header
* of the request. Note that the `access_token` query parameter
* and the `access_token` form parameter are NOT referred to even
* if they exist.
*
* @param AuthleteApi $api
* An implementation of the `AuthleteApi` interface.
*
* @param Request $request
* A request from a client application.
*
* @param string[] $requiredScopes
* Scopes which are required to access the protected resource
* endpoint. This argument is optional and its default value
* is `null`.
*
* @param string $requiredSubject
* Subject (= unique user identifier) which is required to be
* associated with the access token. This argument is optional
* and its default value is `null`.
*
* @return AccessTokenValidator
* A new `AccessTokenValidator` instance.
*/
public static function create(
AuthleteApi $api, Request $request,
array $requiredScopes = null, $requiredSubject = null)

The steps to create an AccessTokenValidator instance shown in the previous example can be written as follows with the create() method.

$validator =
AccessTokenValidator::create(
$api, $request, ['email', 'profile']);

The implementation of create() method extracts the access token from the Authorization header of the request (= the means defined in RFC 6750, 2.1. Authorization Request Header Field).

Let’s protect /api/time API by access tokens.

Modify TimeController.php as follows.

<?php
namespace App\Http\Controllers;

use Authlete\Api\AuthleteApi;
use Authlete\Laravel\Web\AccessTokenValidator;
use Illuminate\Http\Request;

class TimeController extends Controller
{
public function __invoke(Request $request, AuthleteApi $api)
{
$validator = AccessTokenValidator::create(
$api, $request, ['email', 'profile']);

if (!$validator->validate())
{
return $validator->getErrorResponse();
}

return response()->json(getdate());
}
}
?>

Now, /api/time API has been protected by access tokens.

After restarting the web server by php artisan serve --port=8001, access /api/time API without an access token.

$ curl http://127.0.0.1:8001/api/time
$ # Nothing is displayed!

Probably, nothing won’t be displayed. /api/time returned 400 Bad Request and the error message was contained in the WWW-Authenticate header (RFC 6750, 3. The WWW-Authenticate Response Header Field), but the information is not displayed. So, redo curl command with -v option.

$ curl -v http://127.0.0.1:8001/api/time
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> GET /api/time HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Host: 127.0.0.1:8001
< Date: Tue, 22 May 2018 16:36:16 +0000
< Connection: close
< X-Powered-By: PHP/7.1.14
< Cache-Control: no-store, private
< Date: Tue, 22 May 2018 16:36:16 GMT
< Pragma: no-cache
< WWW-Authenticate: Bearer error="invalid_token",
error_description=
"[A057301] The request does not contain a valid access token.",
error_uri=
"https://www.authlete.com/documents/apis/result_codes#A057301"
< Content-Type: text/html; charset=UTF-8
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
<
* Closing connection 0
$

This time, we can confirm that the HTTP status is 400 Bad Request and that the WWW-Authenticate header contains an error message. The error code is invalid_token and the error message says “The request does not contain a valid access token”.

Next, let’s access /api/time API with an access token which does not have email scope and profile scope. We are going to confirm that the access is rejected.

If you follow the steps written in “3.10.1. Access token request by the implicit flow”, an access token that has no scope is created. So, let’s use it.

To use the means written in 2.1. Authorization Request Header Field, add -H option to curl command like the following.

-H 'Authorization: Bearer {ACCESS-TOKEN}'

Let’s access the API.

$ curl -v http://127.0.0.1:8001/api/time -H \
'Authorization: Bearer YGhwQUO-9GC5NmnfJ9kVHMgSsnkiTFcg5hoRt5NK5DM'
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> GET /api/time HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: Bearer YGhwQUO-9GC5NmnfJ9kVHMgSsnkiTFcg5hoRt5NK5DM
>
< HTTP/1.1 403 Forbidden
< Host: 127.0.0.1:8001
< Date: Tue, 22 May 2018 17:05:11 +0000
< Connection: close
< X-Powered-By: PHP/7.1.14
< Cache-Control: no-store, private
< Date: Tue, 22 May 2018 17:05:11 GMT
< Pragma: no-cache
< WWW-Authenticate: Bearer error="insufficient_scope",
error_description=
"[A064301] No scopes are associated with the access token.",
error_uri=
"https://www.authlete.com/documents/apis/result_codes#A064301",
scope="profile email"
< Content-Type: text/html; charset=UTF-8
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
<
* Closing connection 0
$

As expected, we got an error again. The error code is insufficient_scope, which indicates that the access token does not have necessary scopes to access the API. The value of the error response parameter, insufficient_scope, is defined in 3.1. Error Codes of RFC 6750.

Also, note that the WWW-Authenticate header has scope="profile email" at the end. This indicates that profile scope and email scope are required to access the API.

You may have realized that if you want to make your APIs strictly conform to RFC 6750, you have to follow minute rules to build the value of the WWW-Authenticate header when an API returns an error. To be honest, this is a bothersome task. However, the task will become much easier if you use Authlete. It’s because the implementation of Authlete’s /api/auth/introspection generates the value of the WWW-Authenticate header as necessary. This is a major difference between the standard introspection API (RFC 7662) and Authlete’s /api/auth/introspection API. Authlete’s introspection API does introspection, validation and error message building.

Finally, let’s access /api/time API with an access token which has necessary scopes.

First, create an access token which has email scope and profile scope. If you use the implicit flow, input the following URL at the address bar of your browser. Note that the URL has the scope parameter.

http://127.0.0.1:8000/authorization?response_type=token&client_id={CLIENT-ID}&scope=email+profile

If you want to complete the task on the command line, the resource owner password credentials flow can be used.

$ curl -v http://127.0.0.1:8000/api/token \
-d grant_type=password \
-d username={LOGIN-ID} \
-d password={PASSWORD} \
-d client_id={CLIENT-ID} \
-d scope='email profile'
{
"access_token":"2Dde16KZPp1PDmBClM8bQJMmomQUYPjsYLfcB2vqcmU",
"refresh_token":"y5SL5DAFY3rPfRORtgSj31_F2WYw-lDYkXnBY6rgH--",
"scope":"email profile",
"token_type":"Bearer",
"expires_in":86400
}

Let’s access the API with the access token.

$ curl -v http://127.0.0.1:8001/api/time -H \
'Authorization: Bearer 2Dde16KZPp1PDmBClM8bQJMmomQUYPjsYLfcB2vqcmU'
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#0)
> GET /api/time HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: Bearer 2Dde16KZPp1PDmBClM8bQJMmomQUYPjsYLfcB2vqcmU
>
< HTTP/1.1 200 OK
< Host: 127.0.0.1:8001
< Date: Tue, 22 May 2018 18:12:07 +0000
< Connection: close
< X-Powered-By: PHP/7.1.14
< Cache-Control: no-cache, private
< Date: Tue, 22 May 2018 18:12:07 GMT
< Content-Type: application/json
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
<
* Closing connection 0
{
"seconds":7,
"minutes":12,
"hours":18,
"mday":22,
"wday":2,
"mon":5,
"year":2018,
"yday":141,
"weekday":"Tuesday",
"month":"May",
"0":1527012727
}

The output shows that the HTTP status is 200 OK, Content-Type is application/json, and the response body contains expected JSON. The /api/time API returned a successful response.

“UserInfo endpoint” is defined in 5.3. UserInfo Endpoint of OpenID Connect Core 1.0. The endpoint returns user information in JSON or JWT format.

authlete-laravel library includes an implementation of UserInfo endpoint. php artisan authlete:resource-server generates UserInfoController.php under app/Http/Controllers/ and adds routes to routes/api.php. As a result, a UserInfo endpoint is provided at /api/userinfo. Let’s confirm the behavior of the endpoint.

Because a UserInfo endpoint returns information about a user, the implementation of the endpoint must be able to access the user database.

If you followed the steps written in this article for user registration, the user database exists in database/database.sqlite of the authorization server. However, because we created a new separate Laravel project, the database cannot be accessed from the resource server.

So, for simplicity, here we copy database.sqlite from the Laravel project of the authorization server to that of the resource server.

$ cp authorization-server/database/database.sql \
resource-server/database/

Edit .env file of the resource server to make it use SQLite, too.

DB_CONNECTION=sqlite
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=homestead
#DB_USERNAME=homestead
#DB_PASSWORD=secret

To access a UserInfo endpoint, an access token must have at least openid scope.

So, first, let’s create an access token which has openid scope. Since information returned from the UserInfo endpoint will be affected, email scope and profile scope are included here.

http://127.0.0.1:8000/authorization?response_type=token&client_id={CLIENT-ID}&scope=openid+email+profile&redirect_uri=https://api.authlete.com/api/mock/redirection/{SERVICE-API-KEY}

The request above looks similar to the request used in “3.10.2. ID token request by the implicit flow”, but the value of the response_type parameter is not id_token but token.

Let’s access the UserInfo endpoint, /api/userinfo.

$ curl http://127.0.0.1:8001/api/userinfo -H \
'Authorization: Bearer e7wvFGThq2ZFcaXtDdzPhB7Oqbjr5SLfFtIiNmL0PSU'

A successful response will be JSON like the following.

{
"email":"john@example.com",
"name":"John Smith",
"iss":"https://authlete.com",
"sub":"1",
"aud":["7688853985532"],
"exp":253402128000,
"iat":1527022504
}

JWT may be returned depending on the configuration of the client application. See the description about userinfo_signed_response_alg in 2. Client Metadata of OpenID Connect Dynamic Client Registration 1.0 for details.

validate() method of AccessTokenValidator class which was explained in “4.5.3. AccessTokenValidator” calls Authlete’s /api/auth/introspection API every time it is invoked. Therefore, it is affected by network latency.

By caching access token information on the resource server side, the number of API calls to /api/auth/introspection can be reduced, and thus the effect of network latency can be mitigated. Actually, Authlete, Inc. recommends that our customers cache access token information.

Finally

Development of authlete library (for PHP) and authlete-laravel library (for Laravel) took much more time than expected. I realized again that it is a tough task to implement an authorization server and OpenID provider even if a backend system like Authlete exists.

However, the effort of the development has realized the world where a single command, php artisan authlete:authorization-server, can complete the implementation of the basic features of an authorization server and OpenID provider.

Engineering resources, development time and cost, quality, features, reusability, operation and maintenance, following the latest specifications, etc… Considering these, it would be much better to use Authlete with the subscription fee than developing an authorization server and OpenID provider from scratch with trial and error, I believe 😉

Please contact us (sales@authlete.com)!

Co-founder and representative director of Authlete, Inc., working as a software engineer since 1997. https://www.authlete.com/

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store