Definitive Guide

Protected Resource


1. Target Readers

The main purpose to implement OAuth 2.0 is to protect Web APIs by access tokens. Therefore, all of you will finally need to read this document.

What people want to do is to protect their Web APIs by access tokens, but they will soon notice that they have to implement RFC 6749 (OAuth 2.0) as a prerequisite. Authlete exists to mitigate the burden to implement the specification and to enable you to focus on implementing your Web APIs.


2. What Is Protected Resource Endpoint?

Web APIs protected by OAuth 2.0 access tokens are called protected resource endpoints. Such endpoints do the following before they return requested data to client applications.

  1. Extract an access token from a request.
  2. Get detailed information about the access token from the authorization server.
  3. Validate the access token.

The following sections describe how to do these.


3. Extract Access Token

How should a protected resource endpoint accept an access token from a client application? It is possible to devise as many new ways to do it as you like. However, we don't have to reinvent the wheel. RFC 6750 (Bearer Token Usage) has already defined three ways to accept an access token as listed below.

  1. Via Authorization header. (2.1. Authorization Request Header Field)
  2. Via a form parameter access_token. (2.2. Form-Encoded Body Parameter)
  3. Via a query parameter access_token. (2.3. URI Query Parameter)

The most desirable way is the first one and RFC 6750 says "Resource servers MUST support this method."   Simply saying, when a client application uses the first way, a protected resource endpoint can find an HTTP header like below in the request.

Authorization: Bearer access-token

Below are examples to extract an access token.

            private static final Pattern CHALLENGE_PATTERN
                = Pattern.compile("^Bearer *([^ ]+) *$", Pattern.CASE_INSENSITIVE);

            /**
             * Entry point for GET method.
             */
            @GET
            public Response get(
                @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization,
                @QueryParam("access_token") String accessToken)
            {
                // Extract an access token from Authorization header or use
                // the value of query parameter 'access_token'.
                accessToken = extractAccessToken(authorization, accessToken);
            }

            /**
             * Entry point for POST method.
             */
            @POST
            @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
            public Response post(
                @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization,
                @FormParam("access_token") String accessToken)
            {
                // Extract an access token from Authorization header or use
                // the value of form parameter 'access_token'.
                accessToken = extractAccessToken(authorization, accessToken);
            }

            /**
             * Extract an access token from Authorization header, or return
             * the value of 'access_token' request parameter.
             */
            private static String extractAccessToken(
                String authorization, String accessToken)
            {
                // If Authorization header is not found in the request.
                if (authorization == null)
                {
                    // Return the value of 'access_token' request parameter.
                    return accessToken;
                }

                // Pattern matching on the value of Authorization header.
                Matcher matcher = CHALLENGE_PATTERN.matcher(authorization);

                // If the value of Authorization header is in the format of
                // 'Bearer access-token'.
                if (matcher.matches())
                {
                    // Return the value extracted from Authorization header.
                    return matcher.group(1);
                }
                else
                {
                    // Return the value of 'access_token' request parameter.
                    return accessToken;
                }
            }
          
            # Method to extract an access token from a request.
            def extract_access_token(request)
              # The value of Authorization header.
              header = request.env["HTTP_AUTHORIZATION"]

              # If the value is in the format of 'Bearer access-token'.
              if /^Bearer[ ]+(.+)/i =~ header
                # Return the value extracted from Authorization header.
                return $1
              end

              # Return the value of 'access_token' request parameter.
              return request["access_token"]
            end

            # Entry point for GET method.
            get '/hello' do
              # Extract an access token from the request.
              access_token = extract_access_token(request)
            end

            # Entry point for POST method.
            post '/hello' do
              # Extract an access token from the request.
              access_token = extract_access_token(request)
            end
          
            /**
             * Function to extract an access token from a request.
             */
            function extract_access_token()
            {
                // The value of Authorization header.
                $header = $_SERVER['HTTP_AUTHORIZATION'];

                // If the value is in the format of 'Bearer access-token'.
                if ($header != null && preg_match('/^Bearer[ ]+(.+)/i', $header, $captured))
                {
                    // Return the value extracted from Authorization header.
                    return $captured;
                }

                if ($_SERVER['REQUEST_METHOD'] == 'GET')
                {
                    // Return the value of 'access_token' query parameter.
                    return $_GET['access_token'];
                }
                else
                {
                    // Return the value of 'access_token' form parameter.
                    return $_POST['access_token'];
                }
            }
          

4. Introspect Access Token

4.1. RFC 7662

The process to get detailed information about an access token is called introspection. How to introspect an access token depends on each implementation of OAuth 2.0 authorization servers.

However, in Oct. 2015, RFC 7662 (OAuth 2.0 Token Introspection) was released as a standard specification for introspection. If an authorization server you are using supports RFC 7662, your protected resource endpoints can introspect access tokens without depending on a specific authorization server implementation. But still, how to protect the introspection endpoint itself depends on each implementation as mentioned in "2.1. Introspection Request" of "RFC 7662".

From RFC 7662, 2.1. Introspection Request


To prevent token scanning attacks, the endpoint MUST also require some form of authorization to access this endpoint, such as client authentication as described in OAuth 2.0 [RFC 6749] or a separate OAuth 2.0 access token such as the bearer token described in OAuth 2.0 Bearer Token Usage [RFC 6750]. The methods of managing and validating these authentication credentials are out of scope of this specification.


4.2. Authlete Introspection API

Authlete provides an introspection API at /api/auth/introspection. The implementation of the API does not comply with RFC 7662, but from a viewpoint of those who develop protected resource endpoints, our API is better than RFC 7662 as listed below.

  1. You can delegate validation to our introspection API.
  2. Our introspection API generates an error message that complies with RFC 6750 when a given access token is invalid.

Simply saying, Authlete's introspection API does introspection + validation + building an error message complying with RFC 6750.

You can find the detailed specification of Authlete's introspection API in Authlete Web APIs document. Below is just a short summary.

HTTP method POST
Content-Type application/x-www-form-urlencoded or application/json
Security Basic Authentication with API credentials of a service.
Parameters Name Required Description
token YES An access token to introspect.
scopes NO Permissions your protected resource endpoint requires.
subject NO End-user's unique identifier your protected resource endpoint requires.
Content-Type application/json
Parameters Name Type Description
resultCode string Authlete's result code.
resultMessage string Summary of the result.
action string The next action your protected resource endpoint should take.
clientId number The ID of the client application.
subject string The unique identifier of the end-user associated with the access token.
scopes string array Permissions associated with the access token.
existent boolean true if the access token exists.
usable boolean true if the access token has not expired.
sufficient boolean true if the access token covers all the required permissions.
refreshable boolean true if the access token can be refreshed using its associated refresh token.
responseContent string An error message to be used as the value of WWW-Authenticate header.

4.3. Authlete Introspection API Call

The following are examples of method/functiton implementations to call Authlete's introspection API.

                    #--------------------------------------------------
                    # Class representing a Web response
                    #--------------------------------------------------
                    class WebResponse
                      # Constructor with an HTTP status and an entity body.
                      def initialize(status, body = nil)
                        # HTTP status
                        @status = status

                        # Entity body
                        @body = body

                        # HTTP headers
                        @headers = {
                          "Cache-Control" => "no-store",
                          "Pragma"        => "no-cache"
                        }
                      end

                      # Set an HTTP header.
                      def set_header(name, value)
                        @headers[name] = value

                        return self
                      end

                      # Set "application/json".
                      def json
                        return set_header("Content-Type", "application/json;charset=UTF-8")
                      end

                      # Set "text/plain".
                      def plain
                        return set_header("Content-Type", "text/plain;charset=UTF-8")
                      end

                      # Set "text/html".
                      def html
                        return set_header("Content-Type", "text/html;charset=UTF-8")
                      end

                      # Set Location header.
                      def location(location)
                        return set_header("Location", location)
                      end

                      # Set WWW-Authenticate header.
                      def wwwAuthenticate(challenge)
                        return set_header("WWW-Authenticate", challenge)
                      end

                      # Create an array containing an HTTP status, HTTP headers
                      # and an entity body, which is suitable as an object returned
                      # from sinatra endpoints.
                      def to_response
                        return [@status, @headers, @body]
                      end

                      # Create a WebException containing this WebResponse.
                      def to_exception
                        return WebException.new(self)
                      end
                    end
                  
                    #--------------------------------------------------
                    # Exception having a Web response
                    #--------------------------------------------------
                    class WebException < StandardError
                      # Constructor with a Web response.
                      def initialize(response)
                        @response = response
                      end

                      # Create an array containing an HTTP status, HTTP headers
                      # and an entity body, which is suitable as an object returned
                      # from sinatra endpoints.
                      def to_response
                        return @response.to_response
                      end
                    end
                  
                    #--------------------------------------------------
                    # Call Authlete's API.
                    #--------------------------------------------------
                    def call_api(path, parameters)
                      # Build the payload.
                      payload = JSON.generate(parameters)

                      begin
                        # Call Authlete's API.
                        response = RestClient::Request.new(
                          :url      => $AUTHLETE_BASE_URL + path,
                          :method   => :post,
                          :user     => $SERVICE_API_KEY,
                          :password => $SERVICE_API_SECRET,
                          :headers  => { :content_type => :json },
                          :payload  => payload
                        ).execute
                      rescue => e
                        begin
                          # Use "resultMessage" if the response can be parsed as JSON.
                          message = JSON.parse(e.response.to_str)["resultMessage"]
                        rescue
                          # Build a generic error message.
                          message = "Authlete's #{path} API failed."
                        end

                        # The API call failed.
                        raise WebResponse.new(500, message).plain.to_exception;
                      end

                      # The response from the API is JSON.
                      return JSON.parse(response.to_str)
                    end
                  
                    #--------------------------------------------------
                    # Call Authlete's /api/auth/introspection API.
                    #--------------------------------------------------
                    def call_introspection_api(token, scopes, subject)
                      return call_api("/api/auth/introspection", {
                        "token"   => token,
                        "scopes"  => scopes,
                        "subject" => subject
                      })
                    end
                  
                    /**
                     * A class representing a Web response.
                     */
                    class WebResponse
                    {
                        private $status;
                        private $body;
                        private $headers;

                        public function __construct($status, $body = null)
                        {
                            $this->status  = $status;
                            $this->body    = $body;
                            $this->headers = [
                                'Cache-Control' => 'no-store',
                                'Pragma'        => 'no-cache'
                            ];
                        }

                        // Set an HTTP header.
                        private function set_header($name, $value)
                        {
                            $this->headers[$name] = $value;

                            return $this;
                        }

                        // Set Content-Type.
                        private function set_content_type($content_type)
                        {
                            return $this->set_header("Content-Type", "{$content_type};charset=UTF-8");
                        }

                        // Set "application/json".
                        public function json()
                        {
                            return $this->set_content_type('application/json');
                        }

                        // Set "text/plain".
                        public function plain()
                        {
                            return $this->set_content_type('text/plain');
                        }

                        // Set "text/html".
                        public function html()
                        {
                            return $this->set_content_type('text/html');
                        }

                        // Set Location header.
                        public function location($location)
                        {
                            return $this->set_header('Location', $location);
                        }

                        // Set WWW-Authenticate header.
                        public function wwwAuthenticate($challenge)
                        {
                            return $this->set_header('WWW-Authenticate', $challenge);
                        }

                        public function finish()
                        {
                            $this->write_response();
                            exit();
                        }

                        private function write_response()
                        {
                            http_response_code($this->status);

                            foreach ($this->headers as $name => $value)
                            {
                                header("{$name}: {$value}");
                            }

                            if ($this->body != null)
                            {
                                print($this->body);
                            }
                        }
                    }
                  
                    /**
                     * Call Authlete's API.
                     */
                    function call_api($path, $parameters)
                    {
                        global $AUTHLETE_BASE_URL;
                        global $SERVICE_API_KEY;
                        global $SERVICE_API_SECRET;

                        $curl = curl_init($AUTHLETE_BASE_URL . $path);

                        curl_setopt_array($curl, [
                            CURLOPT_POST           => 1,
                            CURLOPT_HTTPAUTH       => CURLAUTH_BASIC,
                            CURLOPT_USERPWD        => $SERVICE_API_KEY . ':' . $SERVICE_API_SECRET,
                            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
                            CURLOPT_POSTFIELDS     => json_encode($parameters),
                            CURLOPT_RETURNTRANSFER => 1
                        ]);

                        $body  = curl_exec($curl);
                        $errno = curl_errno($curl);
                        $error = curl_error($curl);

                        curl_close($curl);

                        if (CURLE_OK !== $errno)
                        {
                            // The error message.
                            $message = "Authlete's {$path} API failed: {$error}";

                            // Try to parse the response as JSON.
                            $json = json_decode($body, true);

                            // If the response was parsed as JSON successfully.
                            if (JSON_ERROR_NONE === json_last_error())
                            {
                                // Use 'resultMessage' as the error message.
                                $message = $json['resultMessage'];
                            }

                            error_log($message, 0);

                            // The API call failed.
                            (new WebResponse(500, $message))->plain()->finish();
                            return null; // Not reach here.
                        }

                        // The response from the API is JSON.
                        $json = json_decode($body, true);

                        // The result message of the API call.
                        //error_log("Authlete's {$path} API result: {$json['resultMessage']}");

                        return $json;
                    }
                  
                    /**
                     * Call Authlete's /api/auth/introspection API.
                     */
                    function call_introspection_api($token, $scopes, $subject)
                    {
                        return call_api('/api/auth/introspection', [
                            'token'   => $token,
                            'scopes'  => $scopes,
                            'subject' => $subject
                        ]);
                    }
                  
                    // Configuration to call Authlete Web APIs.
                    private static final String AUTHLETE_BASE_URL   = "https://api.authlete.com";
                    private static final String SERVICE_API_KEY     = "Your-Service-API-Key";
                    private static final String SERVICE_API_SECRET  = "Your-Service-API-Secret";
                    private static final AuthleteConfiguration CONF = new AuthleteSimpleConfiguration()
                        .setBaseUrl(AUTHLETE_BASE_URL)
                        .setServiceApiKey(SERVICE_API_KEY)
                        .setServiceApiSecret(SERVICE_API_SECRET);

                    // Authlete Web APIs.
                    private static final AuthleteApi API = AuthleteApiFactory.getInstance(CONF);
                  
                    /**
                     * Call Authlete's /api/auth/introspection API.
                     */
                    private static IntrospectionResponse callIntrospectionApi(
                            String accessToken, String[] scopes, String subject)
                    {
                        // Request to /api/auth/introspection API.
                        IntrospectionRequest request = new IntrospectionRequest()
                            .setToken(accessToken)
                            .setScopes(scopes)
                            .setSubject(subject);

                        try
                        {
                            // Call /api/auth/introspection API.
                            return API.introspection(request);
                        }
                        catch (AuthleteApiException e)
                        {
                            // 500 Internal Server Error
                            throw new WebAppliationException(e, 500);
                        }
                    }
                  
<dependency>
    <groupId>com.authlete</groupId>
    <artifactId>authlete-java-common</artifactId>
    <version>version</version>
</dependency>

<dependency>
    <groupId>com.authlete</groupId>
    <artifactId>authlete-java-client-jaxrs</artifactId>
    <version>version</version>
</dependency>

5. Validate Access Token

The next step after getting detailed information about an access token is to validate the access token. Validation involves the following steps.

  1. Has it expired or not?
  2. Has it been revoked or not?
  3. Does it cover necessary permissions?
  4. Is it associated with the expected end-user?

As mentioned, you can delegate validation to Authlete's introspection API if you want. scopes and subject are optional parameters for the introspection API to specify the scopes (= permissions) that your protected resource endpoint requires and the subject (= unique identifier) of an end-user that your protected resource endpoint expects.

If you utilize scopes parameter and subject parameter accordingly based on your needs, you don't have to write separate validation code.


6. Error Response

If you want to comply with RFC 6750, responses from your protected resource endpoints must contain WWW-Authenticate header on errors. The detailed format of the value of the header is described in "3. The WWW-Authenticate Response Header Field". The format is not so simple as you may guess. Below is an example with parameters mentioned in RFC 6750 (some are optional, though).

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
                  scope="read write",
                  error="invalid_token",
                  error_description="The access token expired",
                  error_uri="http://example.com/error/123"

You might be seduced into ignoring this specification because you may doubt it's worth making an effort just to tell error details to client applications. However, you don't have to make a compromise if you use Authlete's introspection API. It's because a response from the API contains responseContent property whose value can be used as the value of WWW-Authenticate header. As a result, you can easily build an error response like below.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Just embed responseContent here.

7. Dispatch According To Validation Result

If an error is detected on validation, a protected resource endpoint should return 400 Bad Request, 401 Unauthorized or 403 Forbidden (or 500 Internal Server Error) based on the cause of the error.

On the other hand, when an access token is valid, a protected resource endpoint can generate any response it likes. In a typical case, it will return the requested resource in JSON format with 200 OK to the client application.

To make it easy to dispatch based on validation result, a response from Authlete's introspection API contains action property. It is a string that represents the next action you should take. Values of action and their meanings are as follows.

Value Meaning
INTERNAL_SERVER_ERROR An error occurred in Authlete. 500 Internal Server Error should be returned to the client application.
BAD_REQUEST The request from the client application does not contain an access token. 400 Bad Request should be returned to the client application.
UNAUTHORIZED The presented access token does not exist or has expired. 401 Unauthorized should be returned to the client application.
FORBIDDEN The access token does not cover the required scopes or the subject associated with the access token is different from the expected one. 403 Forbidden should be returned to the client application.
OK The access token is valid.

Below are example implementations to dispatch according to validation result. Each method/function implementation (1) accepts an access token [required], a string array of scopes [optional] and a subject [optional], (2) calls the introspection API, and (3) dispatches the flow according to the value of action property in the response from the introspection API. When action is not OK, responseContent property in the response from the introspection API is used as the value of WWW-Authenticate header in the response to a client application.

            #--------------------------------------------------
            # Call Authlete's /api/auth/introspection API.
            # A response from the API is returned when the
            # access token is valid. Otherwise, a WebException
            # is raised.
            #--------------------------------------------------
            def do_introspection(token, scopes, subject)
              # Call Authlete's /api/auth/introspection API.
              response = call_introspection_api(token, scopes, subject)

              # The value of 'WWW-Authenticate' header on error.
              content = response["responseContent"]

              # "action" denotes the next action.
              case response["action"]
              when "INTERNAL_SERVER_ERROR"
                # 500 Internal Server Error
                #   The API request from this implementation was wrong
                #   or an error occurred in Authlete.
                raise WebResponse.new(500).wwwAuthenticate(content).to_exception

              when "BAD_REQUEST"
                # 400 Bad Request
                #   The request from the client application does not
                #   contain an access token.
                raise WebResponse.new(400).wwwAuthenticate(content).to_exception

              when "UNAUTHORIZED"
                # 401 Unauthorized
                #   The presented access token does not exist or has expired.
                raise WebResponse.new(401).wwwAuthenticate(content).to_exception

              when "FORBIDDEN"
                # 403 Forbidden
                #   The access token does not cover the required scopes
                #   or the subject associated with the access token is
                #   different.
                raise WebResponse.new(403).wwwAuthenticate(content).to_exception

              when "OK"
                # The access token is valid (= exists and has not expired).
                return response

              else
                # This never happens.
                raise WebResponse.new(500, "Unknown action").plain.to_exception
              end
            end
          
            /**
             * Call Authlete's /api/auth/introspection API.
             * A response from the API is returned when the
             * access token is valid. Otherwise, a WebException
             * is raised.
             */
            function do_introspection($token, $scopes, $subject)
            {
                // Call Authlete's /api/auth/introspection API.
                $response = call_introspection_api($token, $scopes, $subject);

                // he value of 'WWW-Authenticate' header on error.
                $content = $response['responseContent'];

                // "action" denotes the next action.
                switch ($response['action'])
                {
                    case 'INTERNAL_SERVER_ERROR':
                        // 500 Internal Server Error
                        //   The API request from this implementation was wrong
                        //   or an error occurred in Authlete.
                        (new WebResponse(500))->wwwAuthenticate($content)->finish();
                        return null; // Not reach here.

                    case 'BAD_REQUEST':
                        // 400 Bad Request
                        //   The request from the client application does not
                        //   contain an access token.
                        (new WebResponse(400))->wwwAuthenticate($content)->finish();
                        return null; // Not reach here.

                    case 'UNAUTHORIZED':
                        // 401 Unauthorized
                        //   The presented access token does not exist or has expired.
                        (new WebResponse(401))->wwwAuthenticate($content)->finish();
                        return null; // Not reach here.

                    case 'FORBIDDEN':
                        // 403 Forbidden
                        //   The access token does not cover the required scopes
                        //   or the subject associated with the access token is
                        //   different.
                        (new WebResponse(403))->wwwAuthenticate($content)->finish();
                        return null; // Not reach here.

                    case 'OK':
                        // The access token is valid (= exists and has not expired).
                        return $response;

                    default:
                        // This never happens.
                        (new WebResponse(500, "Unknown action"))->plain()->finish();
                        return null; // Not reach here.
                }
            }
          
            /**
             * Call Authlete's /api/auth/introspection API.
             * A response from the API is returned when the
             * access token is valid. Otherwise, a
             * WebApplicationException is raised.
             */
            private static IntrospectionResponse doIntrospection(
                    String accessToken, String[] scopes, String subject)
            {
                // Call Authlete's /api/auth/introspection API.
                IntrospectionResponse response = callIntrospectionApi(accessToken, scopes, subject);

                // The value of 'WWW-Authenticate' header on error.
                String content = response.getResponseContent();

                int status;

                // Check the next action.
                switch (response.getAction())
                {
                    case INTERNAL_SERVER_ERROR:
                        // 500 Internal Server Error.
                        //   The API request from this implementation was wrong
                        //   or an error occurred in Authlete.
                        status = 500;
                        break;

                    case BAD_REQUEST:
                        // 400 Bad Request.
                        //   The request from the client application does not
                        //   contain an access token.
                        status = 400;
                        break;

                    case UNAUTHORIZED:
                        // 401 Unauthorized.
                        //   The presented access token does not exist or has expired.
                        status = 401;
                        break;

                    case FORBIDDEN:
                        // 403 Forbidden.
                        //   The access token does not cover the required scopes
                        //   or the subject associated with the access token is
                        //   different.
                        status = 403;
                        break;

                   case OK:
                        // The access token is valid (= exists and has not expired).
                        return response;

                    default:
                        // This never happens.
                        throw new WebApplicationException("Unknown action", 500);
                }

                Response res = Response
                        .status(status)
                        .header(HttpHeaders.WWW_AUTHENTICATE, content)
                        .build();

                throw new WebApplicationException(res);
            }
          

8. Endpoint Sample

This section shows example implementations to (1) extract an access token and (2) validate the access token in a protected resource endpoint (so-called Web API). Each implementation requires two scopes, read and write.

            get '/hello' do
              # Extract an access token from the request.
              token = extract_access_token(request)

              begin
                # Introspect and validate the access token.
                response = do_introspection(token, ["read", "write"], nil)
              rescue WebException => e
                # The access token is invalid.
                return e.to_response
              end

              # The access token is valid. Write the main code after here.
              ......
            end
          
            <?php
            require('../include/introspection-functions.php');

            // Extract an access token from the request.
            $token = extract_access_token();

            // Introspect and validate the access token.
            $response = do_introspection($token, ['read', 'write'], null);

            // The access token is valid. Write the main code after here.
            ......
            ?>
          
            private static final String[] SCOPES = { "read", "write" };

            @GET
            public Response hello(
                @HeaderParam(HttpHeaders.AUTHORIZATION) String authorization,
                @QueryParam("access_token") String accessToken)
            {
                // Extract an access token from the request.
                accessToken = extractAccessToken(authorization, accessToken);

                // Introspect and validate the access token.
                IntrospectionResponse response = doIntrospection(accessToken, SCOPES, null);

                // The access token is valid. Write the main code after here.
                ......
            }
          

9. Scope

9.1. How Are Scopes Used?

In OAuth context, scope is a technical term and it represents a permission. When a client application makes an authorization request to an authorization endpoint of a service, the application may list scopes it wants using scope parameter. The following is an example authorization request specifying two scopes: read and write.

https://host/authorize?response_type=token&client_id=your-client-id&scope=read+write

After the end-user authorizes the authorization request, the authorization server issues an access token which are associated with the scopes. The client application uses the access token when it accesses protected resource endpoints of the service. The following is an example protected resource request that uses access_token query parameter to pass an access token to a protected resource endpoint.

https://host/profile?access_token=access-token

A protected resource endpoint extracts an access token from the protected resource request and validates the access token. In a typical case, an implementation of protected resource endpoint checks whether the presented access token is associated with scopes which the endpoint requires. The following is a conceptual code to extract and validate an access token.

// Extract an access token from the protected resource request.
access_token = extract_access_token_from_request(request);

// If the access token has expired.
if (access_token.has_expired) {
    return unauthorized;
}

// Scopes associated with the access token.
scopes = access_token.scopes;

// If the access token covers "read" and "write".
if (scopes.contains("read") && scopes.contains("write")) {
    return ok;
} else {
    return forbidden;
}

9.2. Scope Name Definition

A scope name is a case-sensitive ASCII characters. The exact specification is described in "RFC 6749, 3.3. Access Token Scope" as shown below. %x20 (space), %x22 (double quotation mark) and %x5C (back slash) are not allowed to use.

scope-token = 1*( %x21 / %x23-5B / %x5D-7E )

According to the specification, %x2C (comma) can be used for scope names, but Facebook uses commas as the delimiter to list scope names. It is a violation against the specification.


9.3. Standard Scopes

OpenID Connect Core 1.0 has defined some standard scopes. The table below is the list.

Standard scopes defined in OpenID Connect Core 1.0
  Name Section
1 address 5.4. Requesting Claims using Scope Values
2 email 5.4. Requesting Claims using Scope Values
3 openid 3.1.2.1. Authentication Request
4 offline_access 11. Offline Access
5 phone 5.4. Requesting Claims using Scope Values
6 profile 5.4. Requesting Claims using Scope Values

When you start to create a new service using Service Owner Console, these standard scopes are set as the default value of "supported scopes". Among them, openid is the most important one. If you removed openid from the list of supported scopes, features related to OpenID Connect would not work. So, be careful when you remove standard scops.


9.4. Scope Registration

Scopes need to be registered in advance before use. How to register scopes depends on each authorization server implementation.

Authlete provides GUI for you to edit the list of scopes supported by your service. Login Service Owner Console, click "Edit" button of a service whose scopes you want to edit, and click "Token" tab. You can see "supported scopes" at the bottom like below.

Token / Supported Scopes

After editing the list, don't forget to press "Update" to apply changes.


10. Summary

Authlete provides an introspection API (/api/auth/introspection) for implementations of protected resource endpoints. The API is better than RFC 7662 because it does not only introspection but also validation and building an error message complying with RFC 6750.