Securing APIs with OAuth

Learning Objectives

Lesson

In this lesson you will use Auth0, a commercial implementation of the OAuth standard, to secure your Messages API using the programming language of your choice.

Auth0 is used by many well known companies to secure their Web APIs. Auth0 can be used both for authentication of users and authorization to access specific APIs. In this lesson we are going to focus on API authorization via tokens issued by the Auth0 platform.

The diagram below illustrates the OAuth flow we will use to secure our API resource. It consists of a client app requesting a token from Auth0, then passing this token as authorization to access the Messages API:

OAuth Flow sequence diagram showing how an identify provider issues a token which is used to secure a resource

Auth0 sign up and configuration

We firstly need to create an account with Auth0 and configure a new logical API and Application.

  1. Go to https://auth0.com/signup - enter your email and then create a PERSONAL account type in the appropriate domain (EU or US)

  2. Navigate to your Dashboard and select Applications->APIs->Create API. Name your new API MessagesAPI and set the Identifier to be messagesAPI. Note that we are not actually creating a real API here, we are merely assigning a logical name for the API which Auth0 can use to secure access.

    Auth0 Messages API

  3. Click on your new API and navigate to the Test tab. Click on Create & Authorize Test Application. Again, we are not creating a real App here, we are merely assigning a logical name for an Application which will be authorised to access the Messages API.

    You will then be presented with example code (in multiple languages) for your test application to request tokens from Auth0 and to send tokens to the MessagesAPI.

  4. Use the example cURL request to help you construct a Postman request to obtain a new OAuth token.

  5. If all goes well, you should receive a 200 OK and the body of the response should contain a JWT access token. Paste this into https://jwt.io and explore the contents.

    example token response

Assignment

Now we have the ability to request and send tokens, we need to integrate Auth0 with the code for our Messages API created in a previous lesson. Our Messages API was previously secured using Basic Auth, you now need to change the security mechanism to be OAuth.

  1. Secure your API using OAuth - this will require you to research how to use OAuth within your language specific framework. The Additional Resources section of this page details how to set this up for Java (Spring Boot) and JavaScript (Express) developers - for other languages please see https://auth0.com/docs/quickstart/backend/ (for PHP see this link). Note that your Auth0 Domain, Client ID and Client Secret are considered sensitive data therefore you should ensure you use environment variables / configuration files to hold this information and add the file(s) to .gitignore to avoid the data being saved to git.

  2. Use Postman to send a token to an API endpoint. The token must be sent in an HTTP Authorization header of type Bearer Token, for example:

        Authorization: "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjV1SlZpY09mY1FTQ19FRTF2VER2ZyJ9.eyJpc3MiOiJodHRwczovL2Rldi0xdTF5ZzYtbS5ldS5hdXRoMC5jb20vIiwic3ViIjoid1V1cGpaTVZiVlA5VlNaeGp0dElkTnVDeDd4TXhJR1hAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbWFuZHlzYXBpIiwiaWF0IjoxNjI5ODk4NTgwLCJleHAiOjE2Mjk5ODQ5ODAsImF6cCI6IndVdXBqWk1WYlZQOVZTWnhqdHRJZE51Q3g3eE14SUdYIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.FJyDPCgTUiwWw2707A56qwtlsUTJ-qqZDdY0CTW7m7kTqigJ0bayRuAa1KzJDB5HhBdDdLnVv1nzcWrOKvPd-84_51pnax8EOVkkW_vO4RL_zp89tENC9dHtXGMUqXhIwWMtiIjHoF1xJUmLfrlqmR9n3KtZr8cdELlTO7a1uESwCSQtd-iCtJ3mX2hkJ-zudmzHRxhTHGnR5XSQSx50JzKG24WXF7z-wAFuCgTadlyVvngytU0_MjyuS937kxqfDWVROy-xbo696k0iGcaH_fZo5YGkCPhKucx04FeZef52PwBelJZuN9gi6GGvj9JsZSb476Wf9Le2Y8qKwOeg2Q"
    
  3. Verify you are not able to access any /messages routes without a token. You should receive a status code of 401 Unauthorized.

  4. Commit your code to GitHub and notify your coach that this assignment is complete.

Assignment extension tasks

  1. Add the ability to secure individual endpoints based on API scopes. One scope should allow read access i.e. allow access to only the GET /messages and GET /messages/{id?} endpoints and the other scope should allow access to all endpoints. Java developers may find this help for managing scopes for Java developers article useful.

Additional Resources

Integrating OAuth for Javascript developers

  1. To support OAuth, you need to install some specific dependencies:

    DependencyPurpose
    express-jwtProvides Express middleware for validating JWTs
    jwks-rsaUsed to retrieve the public key used for signing the JWT
    corsUsed to support Cross-origin resource sharing (CORS) - a mechanism that allows resources to be requested from another domain outside the domain from which the first resource was served
    dotenvUsed to support the use of environment variables
  2. You will be storing details of your Auth0 account in an .env file hence you must add .env to your .gitignore to avoid the sensitive data being saved to git.

  3. Install the following node package dependencies:
    npm install cors dotenv express-jwt jwks-rsa

  4. Remove any dependency to express-basic-auth

  5. Add the following to the start of your app.js file as follows

    const jwt = require('express-jwt');
    const jwksRsa = require('jwks-rsa');
    const cors = require('cors'); 
    
    require('dotenv').config('.env'); // Note: env vars should not be used in production
    
  6. Add the following line AFTER the call to initialise Express

    app.use(cors());
    
  7. Create a .env file to hold environment variables and add the following entries (substituting in your personal Auth0 domain):

    AUTH0_AUDIENCE=messagesAPI

    AUTH0_DOMAIN=[your domain].eu.auth0.com

  8. Add a function to check for a valid OAuth (JWT) token:

    // create middleware for checking the JWT
    const checkJwt = jwt({
    secret: jwksRsa.expressJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `https://${process.env.AUTH0_DOMAIN}.well-known/jwks.json`
    }),
    audience: process.env.AUTH0_AUDIENCE,
    issuer: `https://${process.env.AUTH0_DOMAIN}`,
    algorithms: ['RS256']
    });
    
    
  9. Secure your API:

    app.get("/messages", checkJwt, (req, res) => {
    

Integrating OAuth for Java developers

You will make use of Spring security libraries which support OAuth.

  1. Add the dependencies to your pom.xml file:

        <properties>
            ...
            <spring-security.version>5.4.2</spring-security.version>
            ...
        </properties>
    
        <dependencies>
            ...
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-oauth2-resource-server</artifactId>
                <version>${spring-security.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-oauth2-jose</artifactId>
                <version>${spring-security.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-config</artifactId>
                <version>${spring-security.version}</version>
            </dependency>
    
  2. Create a new class which checks that the JWT has the correct audience value

    import org.springframework.security.oauth2.core.OAuth2Error;
    import org.springframework.security.oauth2.core.OAuth2TokenValidator;
    import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
    import org.springframework.security.oauth2.jwt.Jwt;
    
    /**
    * Validates that the JWT is intended for our API.
    */
    public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    
        private final String audience;
    
        AudienceValidator(String audience) {
            this.audience = audience;
        }
    
        public OAuth2TokenValidatorResult validate(Jwt jwt) {
            OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
    
            if (jwt.getAudience().contains(audience)) {
                return OAuth2TokenValidatorResult.success();
            }
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
    
  3. Modify your SecurityConfiguration to use OAuth

    @Configuration
    @EnableWebSecurity
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Value("${auth0.audience}")
        private String audience;
    
        @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
        private String issuer;
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            httpSecurity.authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    // ** IMPORTANT! do not use the line below in production apps!! **
                    .and().csrf().disable()
                    .cors()
                    .and().oauth2ResourceServer().jwt();
        }
    
        /**
        * Required to enable CORS - NOT suitable for production code!
        *
        * @return CorsConfigurationSource cors configuration
        */
        @Bean
        public CorsConfigurationSource corsConfigurationSource() {
            final CorsConfiguration configuration = new CorsConfiguration();
    
            // ** IMPORTANT! do not use the line below in production apps!! **
            // ** Specify specific origins instead
            configuration.setAllowedOriginPatterns(Arrays.asList("*"));
            configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
            configuration.setAllowCredentials(true);
            configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
    
            final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
    
            return source;
        }
    
        @Bean
        JwtDecoder jwtDecoder() {
            NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
                    JwtDecoders.fromOidcIssuerLocation(issuer);
    
            OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
            OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
            OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
    
            jwtDecoder.setJwtValidator(withAudience);
    
            return jwtDecoder;
        }
    }
    
  4. Add a new file application.yml under src/main/resources to specify your Auth0 domain and audience

    auth0:
    audience: messagesAPI
    spring:
    security:
        oauth2:
        resourceserver:
            jwt:
            issuer-uri: https://[your Auth0 domain].eu.auth0.com/