OAuth for multiple API products
Let users authenticate once with an OIDC provider for access to multiple API products.
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.
The steps vary depending on the OIDC provider. The following example includes steps for Okta. For other providers, see OIDC and OAuth.
Before you begin
Create your APIs, including the Gloo ApiDocs that describe the stitched schema.
Bundle your APIs into API products by using a route table.
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
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 ausagePlans: dev-portal
label on itspets-api
,users-api
, andstore-api
routes. - The
tracks-rt
route table has ausagePlans: dev-portal
label on itstracks-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"}]
- The
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.
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 the API product ID must have this value, such as http://api.example.com/tracks
. If you use AWS Cognito, update apiProductId
in the route table metadata.
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 theapi.example.com
host domain and delegates its routes to thepetstore-rt
andtracks-rt
route tables that represent your API products. - The
petstore-rt
route table has thepetstore
API product ID. - The
tracks-rt
route table has thetracks
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.
Open the Okta dashboard. If you don’t have an Okta account that you can use, sign up for an Okta developer account.
From the Applications menu, click Applications > Create New App. Note that you might see a Create App Integration button instead.
Select OIDC - OpenID Connect as the sign-in method for your app and Single-Page Application as your application type. Then, click Next.
Enter a name for your app and optionally upload a logo.
For Grant type, check both Authorization Code and Refresh Token.
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.
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
- React starter app:
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.
Click Save to save your changes. You are redirected to the Okta app details page.
From the General tab on the Okta app details page, note the Client ID.
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.
From the navigation menu, click Security > API.
Click the Authorization Server that you want to use, such as
default
.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
.From the metadata URI, search for and save the endpoints that you need as environment variables.
The
token_endpoint
is where to get the OAuth token.export TOKEN_ENDPOINT=https://dev-1234567.okta.com/oauth2/default/v1/token
The
authorization_endpoint
is where to get the PKCE authorization code.export AUTH_ENDPOINT=https://dev-1234567.okta.com/oauth2/default/v1/authorize
The
end_session_endpoint
is where to end the session.export LOGOUT_ENDPOINT=https://dev-1234567.okta.com/oauth2/default/v1/logout
Get the JSON Web Key Set (JWKS) that you use later for an inline access token external auth policy.
From the metadata URI, find the
jwks_uri
endpoint. In a new tab, open this endpoint, such ashttps://dev-1234567.okta.com/oauth2/default/v1/keys
.export OKTA_JWKS_URI=<jwks_uri>
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..."}]}
Return to the authorization server page in Okta.
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 theopenid
scope.
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.- In OAuth/OIDC client, enter the name of your app.
- In Grant type, select Authorization Code.
- In User, enter your username or the name of the user that you want to log in to the frontend developer portal.
- In Scopes, enter
openid
. - Click Preview Token, then flip between the
id_token
andtoken
previews. - 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
orgroup
set. You can edit your profile to include this information, then preview the token again.
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.
From the Okta navigation menu, click Security > API.
Click the Authorization Server that you want to use, such as
default
.From the Scopes tab, click Add Scope.
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 astracks
. 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 onhttp://api.example.com/
, then both the scope and the API product ID must have this value, such ashttp://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.
- Name: Enter the host domain with the exact name of the API product ID that you previously retrieved from the
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.
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
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
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.
Setting Description spec.applyToRoutes Select 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.server The external auth server to use for the policy. glooAuth.configs.oauth2.accessTokenValidation.jwt Set up the policy to enforce authentication via an access token that conforms to the JWT specification. glooAuth.configs.oauth2.accessTokenValidation.jwt.remoteJwks.url Use 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 theremoteJwks.url
field with thelocalJwks.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.opaAuth Configure the OPA authentication details. glooAuth.configs.opaAuth.modules Refer 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.query The 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 totrue
.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 theallow
conditions in the policy.Verify that the external auth policy is applied successfully.
- 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
- Get the authconfig resource that was created for your policy and make sure that it shows
ACCEPTED
.If you see akubectl get authconfig -n gloo-mesh -o yaml
REJECTED
error similar toinvalid character 'k' looking for beginning of object key string
, try copying the$CERT_KEYS
value manually again.
- Review the status of the external auth policy and make sure that it shows
Verify OAuth
Verify that requests to your API products now require external authentication.
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-22 8081
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
Review the output of the previous step. The request does not succeed because you need to authenticate via OIDC.
Get an access token from Okta that includes the API product ID in the scope.
- The
CLIENT_ID
andCLIENT_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 ashttps://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
- The
Save the token as an environment variable.
export ACCESS_TOKEN=<access-token>
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, ...
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
- If you haven’t already, set rate limits for your API products.
- Configure the developer portal.