Let users authenticate once with an OpenID Connect (OIDC) identity provider (IdP) for access to multiple API products. This guide uses Okta as an example, but you can adapt the steps to your OIDC provider.

In the OIDC provider, you configure an app with credentials that you give the Gloo external auth service to handle the OAuth flow for you. As part of this configuration, you create a custom scope with the API product ID of each product. This way, the IdP returns an access token with the scope for all the API product IDs that you want to grant access to. Gloo uses the API product ID scope to validate and grant access to an individual API product.

You configure the API product ID when you bundle your APIs together in a Gloo route table. To protect your APIs, you can also label the route table so that you can apply an external auth policy to it. The external auth policy includes the details for how to retrieve the access token from the IdP, as well as rules for allowing or denying requests. In this guide, you write an Open Policy Agent (OPA) policy to check the API product IDs in a JWT access token.

For more information about this type of external auth, see OIDC and OAuth.

Before you begin

  1. Create your APIs, including the Gloo ApiDocs that describe the stitched schema.

  2. Bundle your APIs into API products by using a route table.

  3. Make sure that the external auth service is installed and running. If not, install the external auth service in your single or multicluster environment.

      kubectl get pods -A -l app=ext-auth-service
      
  4. Get the labels of your routes to use to apply policies to, such as with the example query.

      kubectl get rt -n gloo-mesh-gateways -o=jsonpath='{range .items[*]}[{.metadata.name}, {.spec.http[*].name}, {.spec.http[*].labels}]{"\n"}{end}'
      

    Example output:

    • The api-example-com-rt route table does not have any route-level labels. To apply policies, you can add labels to those routes.
    • The petstore-rt route table has a usagePlans: dev-portal label on its pets-api, users-api, and store-api routes.
    • The tracks-rt route table has a usagePlans: dev-portal label on its tracks-api route.
      [api-example-com-rt, , ]
    [petstore-rt, pets-api users-api store-api, {"usagePlans":"dev-portal"} {"usagePlans":"dev-portal"} {"usagePlans":"dev-portal"} ]
    [tracks-rt, tracks-api, {"usagePlans":"dev-portal"}]
      

Step 1: Get the API product ID

Get each API product ID that you want to enforce authentication to. The API product ID is set in the route table’s portalMetadata.apiProductId field. In the following example, you get the petstore and tracks API product IDs.

  kubectl get rt -n gloo-mesh-gateways -o=jsonpath='{range .items[*]}[{.metadata.name}, {.spec.portalMetadata.apiProductId}]{"\n"}{end}'
  
  • The api-example-com-rt route table does not have an API product ID. This route table is used to set the api.example.com host domain and delegates its routes to the petstore-rt and tracks-rt route tables that represent your API products.
  • The petstore-rt route table has the petstore API product ID.
  • The tracks-rt route table has the tracks API product ID.
  [api-example-com-rt, ]
[petstore-rt, petstore]
[tracks-rt, tracks]
  

Step 2: Set up an OIDC app

Configure an Okta OIDC app to get the information that you need to create external auth policies to secure your resources.

  1. Open the Okta dashboard. If you don’t have an Okta account that you can use, sign up for an Okta developer account.

  2. From the Applications menu, click Applications > Create New App. Note that you might see a Create App Integration button instead.

    Figure: Okta application dashboard
    Figure: Okta application dashboard
    Figure: Okta application dashboard
    Figure: Okta application dashboard

  3. Select OIDC - OpenID Connect as the sign-in method for your app and Single-Page Application as your application type. Then, click Next.

  4. Enter a name for your app and optionally upload a logo.

  5. For Grant type, check both Authorization Code and Refresh Token.

  6. For Sign-in redirect URIs, enter the location from which you want to allow users to log in. The URL is composed of hostname that you set up for your Portal resources and the /callback path. For example, for the developer portal frontend app, you might enter:

    • https://developer.example.com to let a user log in from the secured home page.
    • React starter app: https://developer.example.com/apis to let a user log in from the APIs page.
    • Backstage frontend plug-in: https://developer.example.com/gloo-platform-portal to let a user log in from the Gloo Portal Backstage plug-in page.
    • https://developer.example.com/callback for the callback path.
  7. For the Sign-out redirect URIs, enter the location to redirect the user after logging out, such as the following examples:

    • React starter app: https://developer.example.com/logout.
    • Backstage frontend plug-in: https://developer.example.com/gloo-platform-portal/logout
  8. From the Assignments section, select Allow everyone in your organization to access. This way, you do not need to assign a user or group to this app. Instead, you can use your Okta developer account credentials to test the Okta authentication flow.

  9. Click Save to save your changes. You are redirected to the Okta app details page.

  10. From the General tab on the Okta app details page, note the Client ID.

    Figure: Okta General tab
    Figure: Okta General tab
    Figure: Okta General tab
    Figure: Okta General tab

  11. Store the Client ID as an environment variable.

      export CLIENT_ID=<client-id>
      

Step 3: Configure other OIDC account details

Configure other Okta account details, such as the claims that you want to include in the access token.

  1. From the navigation menu, click Security > API.

  2. Click the Authorization Server that you want to use, such as default.

  3. From the Settings tab, click the Metadata URI. In a new tab, your browser opens to a URL similar to https://dev-1234567.okta.com/oauth2/default/.well-known/oauth-authorization-server.

  4. From the metadata URI, search for and save the endpoints that you need as environment variables.

    1. The token_endpoint is where to get the OAuth token.

        export TOKEN_ENDPOINT=https://dev-1234567.okta.com/oauth2/default/v1/token
        
    2. The authorization_endpoint is where to get the PKCE authorization code.

        export AUTH_ENDPOINT=https://dev-1234567.okta.com/oauth2/default/v1/authorize
        
    3. The end_session_endpoint is where to end the session.

        export LOGOUT_ENDPOINT=https://dev-1234567.okta.com/oauth2/default/v1/logout
        
  5. Get the JSON Web Key Set (JWKS) that you use later for an inline access token external auth policy.

    1. From the metadata URI, find the jwks_uri endpoint. In a new tab, open this endpoint, such as https://dev-1234567.okta.com/oauth2/default/v1/keys.

        export OKTA_JWKS_URI=<jwks_uri>
        
    2. Copy and save the entire value of these keys as an environment variable.

        export CERT_KEYS={"keys":[{"kty":"RSA","alg":"RS256","kid":"sKv...","use":"sig","e":"AQAB","n":"kdhR..."}]}
        
  6. Return to the authorization server page in Okta.

  7. From the Claims tab, click Add Claim to add any claims that you want the access token to include. Later, you can use these claims in your portal groups. For example, the portal group might require a member of organization: solo.io to access certain API products. For more information about claims, see JWT structure. For example, you might configure the following claims:

    • email: Return the user’s email as configured in the user’s profile.
    • organization: Return the user’s organization as configured in the user’s profile.
    • group: Include any group (match regex *) for the openid scope.

    Figure: Okta default auth server Claims tab
    Figure: Okta default auth server Claims tab
    Figure: Okta default auth server Claims tab
    Figure: Okta default auth server Claims tab

  8. From the Token Preview tab, verify that the tokens return the information that you expect, such as the same kid as $CERT_KEYS value that you previously saved and the claims that you configured.

    1. In OAuth/OIDC client, enter the name of your app.
    2. In Grant type, select Authorization Code.
    3. In User, enter your username or the name of the user that you want to log in to the frontend developer portal.
    4. In Scopes, enter openid.
    5. Click Preview Token, then flip between the id_token and token previews.
    6. If you do not see the claim information that you expect, click your profile > My settings and review your personal information. For example, you might not have an organization or group set. You can edit your profile to include this information, then preview the token again.

    Figure: Okta Token Preview tab
    Figure: Okta Token Preview tab
    Figure: Okta Token Preview tab
    Figure: Okta Token Preview tab

Step 4: Add the API product ID as a scope

Create a custom scope with the API product ID of each product. This way, the IdP returns an access token with the scope for all the API product IDs that you want to grant access to. Gloo uses the API product ID scope to validate and grant access to an individual API product.

  1. From the Okta navigation menu, click Security > API.

  2. Click the Authorization Server that you want to use, such as default.

  3. From the Scopes tab, click Add Scope.

  4. Fill out the form to add a scope for the API product that you want to grant access to.

    • Name: Enter the host domain with the exact name of the API product ID that you previously retrieved from the portalMetadata.apiProductId field in the route table that you use to expose your APIs, such as tracks. Note that if this value does not match the API product ID, authentication fails. Keep in mind that some IdPs such as AWS Cognito require that the API product ID for the scopes in the IdP-provided token include the URI of the resource server. For example, if your API is exposed on http://api.example.com/, then both the scope and the API product ID must have this value, such as http://api.example.com/tracks.
    • Display phrase: Especially if your API product ID is not human-friendly, give a descriptive phrase for the API product, such as Tracks API product.
    • Description: Optionally enter a description of the API product, such as to note that this scope is to authenticate with multiple API products in your developer portal.

    Figure: Okta add scopes modal
    Figure: Okta add scopes modal
    Figure: Okta add scopes modal
    Figure: Okta add scopes modal

  5. Repeat the previous step for each API product that you want to grant access to, such as petstore.

Step 5: Create the OAuth policy with OPA

Create Rego and OAuth policies to enforce authentication to your API products. The following example uses JWT access token validation. If you want to use a different method of access token validation such as introspection, you must adjust your OPA Rego policy accordingly.

  1. Create an OPA Rego policy as a Kubernetes config map in the same cluster where you create the external auth policy. The example Rego policy grants access to API products when the access token from the OIDC provider has a scope that matches the API product ID. For more information about Rego policies, see OPA.

      kubectl apply -f - <<EOF
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: oauth-scope-apiproduct-opa-cm
      namespace: gloo-mesh
    data:
      policy.rego: |-
        package test
    
        default allow = false
    
        allow {
          # Get the API product ID of the API product that the
          # user is trying to access from the request metadata
          filter_metadata := input.check_request.attributes.metadata_context.filter_metadata
          apimanagement_metadata := filter_metadata["io.solo.gloo.apimanagement"]
          api_product_id := apimanagement_metadata["api_product_id"]
    
          # Get the scopes from the access token
          scopes := split(input.state.jwtAccessToken.scope, " ")
          scope := scopes[_]
    
          # Ensure API product ID and scopes are not empty
          api_product_id != ""
          scope != ""
    
          # Validate that the scope includes the API product ID
          # of the product that the user is accessing
          scope == api_product_id
        }
    EOF
      
  2. Create an external auth server that enforces your extauth policy.

      kubectl apply -f - <<EOF
    apiVersion: admin.gloo.solo.io/v2
    kind: ExtAuthServer
    metadata:
      name: oauth-okta-server
      namespace: gloo-mesh
    spec:
      destinationServer:
        ref:
          cluster: $CLUSTER_NAME
          name: ext-auth-service
          namespace: gloo-mesh
        port:
          name: grpc
    EOF
      
  3. Create an OAuth external auth policy to enforce authentication via an access token from the Okta OIDC provider that you set up. Make sure that the label you use to apply the policy matches the labels on the routes that you want to protect, such as usagePlans: "dev-portal".

      kubectl apply -f -<<EOF
    apiVersion: security.policy.gloo.solo.io/v2
    kind: ExtAuthPolicy
    metadata:
      name: oauth-okta
      namespace: gloo-mesh
    spec:
      applyToRoutes:
        - route:
            labels:
              usagePlans: "dev-portal"
      config:
        server:
          name: oauth-okta-server
          namespace: gloo-mesh
          cluster: $CLUSTER_NAME
        glooAuth:
           configs:
             - oauth2:
                 accessTokenValidation:
                   jwt:
                     remoteJwks:
                       url: <https://dev-1234567.okta.com/oauth2/default/v1/keys>
                     dynamicMetadataFromClaims:
                       usagePlans: usagePlan
             - opaAuth:
                 modules:
                 - name: oauth-scope-apiproduct-opa-cm
                   namespace: gloo-mesh
                 query: "data.test.allow == true" 
    EOF
      

    Review the following table to understand this configuration. For more information, see the API docs.

    SettingDescription
    spec.applyToRoutesSelect the routes that you want to apply this policy to. In this example, you want to require external authentication for all routes with the usagePlans: "dev-portal" label.
    spec.config.serverThe external auth server to use for the policy.
    glooAuth.configs.oauth2.accessTokenValidation.jwtSet up the policy to enforce authentication via an access token that conforms to the JWT specification.
    glooAuth.configs.oauth2.accessTokenValidation.jwt.remoteJwks.urlUse the remote JWKS that you provide inline to validate the JWT access token. You can retrieve this value from the authorization server’s well-known metadata URI. The remote JWKS value is often in a field such as jwks_uri, similar to the following: https://dev-1234567.okta.com/oauth2/default/v1/keys. For quick testing, you can also replace the remoteJwks.url field with the localJwks.inlineString setting instead, and provide the $CERT_KEYS with the entire {"keys":[{"kid":"_YYA...","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"r4AXlC9sR..."}]} value that you retrieved when you set up Okta.
    glooAuth.configs.opaAuthConfigure the OPA authentication details.
    glooAuth.configs.opaAuth.modulesRefer to the name and namespace of the config map that has the OPA policy. Then, Gloo Mesh Gateway can use the OPA policy to resolve the query. This example uses the config map that you previously created.
    glooAuth.configs.opaAuth.queryThe query that determines the authentication decision. The result of this query must be either a boolean or an array with a boolean as the first element. A value of true means that the request is authorized. Any other value or error means that the request is denied. In this example, data.test.allow is set to true. data is the section in the config map. test.allow is part of the OPA policy that you previously created. Access is allowed only if the response meets the allow conditions in the policy.
  4. Verify that the external auth policy is applied successfully.

    1. Review the status of the external auth policy and make sure that it shows ACCEPTED.
        kubectl get extauthpolicy oauth-okta -n gloo-mesh -o yaml
        
    2. Get the authconfig resource that was created for your policy and make sure that it shows ACCEPTED.
        kubectl get authconfig -n gloo-mesh -o yaml
        
      If you see a REJECTED error similar to invalid character 'k' looking for beginning of object key string, try copying the $CERT_KEYS value manually again.

Verify OAuth

Verify that requests to your API products now require external authentication.

  1. Get the external address of your ingress gateway. The steps vary depending on the type of load balancer that backs the ingress gateway.

      export INGRESS_GW_ADDRESS=$(kubectl get svc -n gloo-mesh-gateways istio-ingressgateway -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
    echo $INGRESS_GW_ADDRESS
      

    Note: Depending on your environment, you might see <pending> instead of an external IP address. For example, if you are testing locally in kind or minikube, or if you have insufficient permissions in your cloud platform, you can instead port-forward the service port of the ingress gateway:

      kubectl -n gloo-mesh-gateways port-forward deploy/istio-ingressgateway-1-23 8081
      
  2. Send a curl request to one of your APIs.

      curl -v --resolve api.example.com:80:${INGRESS_GW_IP} http://api.example.com/trackapi/tracks
      
  3. Review the output of the previous step. The request does not succeed because you need to authenticate via OIDC.

  4. Get an access token from Okta that includes the API product ID in the scope.

    • The CLIENT_ID and CLIENT_SECRET are from the Okta app that you configured.
    • The scope is the scope that you configured in the Okta authorization server that matches your API product ID, such as https://api.example.com/tracks.
    • The TOKEN_ENDPOINT is where to get the access token from the authorization server that you configured, such as https://dev-1234567.okta.com/oauth2/default/v1/token.
      curl -X POST -H "Accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" \
     --data-urlencode "client_id=$CLIENT_ID" \
     --data-urlencode "client_secret=$CLIENT_SECRET" \
     --data-urlencode "grant_type=client_credentials" \
     --data-urlencode "scope=https://api.example.com/tracks" \
     $TOKEN_ENDPOINT
      
  5. Save the token as an environment variable.

      export ACCESS_TOKEN=<access-token>
      
  6. Repeat the request to your API. Now, the request succeeds!

      curl -v -H "Authorization: Bearer $ACCESS_TOKEN" --resolve api.example.com:80:${INGRESS_GW_IP} http://api.example.com/trackapi/tracks
      

    Example output:

      < HTTP/1.1 200 OK
    ...
    [
      {
        "id": "c_0",
        "thumbnail": "https://res.cloudinary.com/dety84pbu/image/upload/v1598465568/nebula_cat_djkt9r.jpg",
        "topic": "Cat-stronomy",
        "authorId": "cat-1",
        "title": "Cat-stronomy, an introduction",
        "description": "Curious to learn what Cat-stronomy is all about? Explore the planetary and celestial alignments and how they have affected our space missions.",
        "numberOfViews": 163,
        "createdAt": "2018-09-10T07:13:53.020Z",
        "length": 2377,
        "modulesCount": 10,
        ...  
      
  7. Send a similar request to a different API product. Because the access token includes the API product IDs for both API products, this request also succeeds.

      curl -v -H "Authorization: Bearer $ACCESS_TOKEN" --resolve api.example.com:80:${INGRESS_GW_IP} http://api.example.com/petstore/pet
      

    Example output:

      < HTTP/1.1 200 OK
    ...
    [
      {
        "id": 1,
        "name": "Barky",
        "photoUrls": [
          "image1.jpg"
        ],
    ...
      

Good job! Your API products are now protected by the OAuth policies. Later, you can set up the frontend for the developer portal. Then, you are able to securely log in and get access to the API products again.

Next steps

When you are done trying out Portal, you can clean up all of the resources that you created.