Protect OutSystems REST APIs using OpenID Connect

Stefan Weber
ITNEXT
Published in
12 min readOct 16, 2022

--

In this article, we’ll look at how to protect exposed OutSystems REST API endpoints using OpenID Connect.

OpenID Connect is an identity layer based on the OAuth 2.0 framework. It allows applications (clients) to authenticate users. The actual authentication is performed by a central service called an OpenID provider. After successful authentication, the provider issues an identity, access, and refresh token. The access token can be used to authorize an application to access resources (APIs).

This is of course just a rough explanation and i strongly recommend building up a good understanding of OAuth 2.0 and OpenID Connect before applying it in production environments. An excellent starting point is the following article by Travis Spencer — CEO of the Identity Management Platform provider curity.io.

Curity publishes articles here on medium.com on diverse topics of identity and access management.

For this walkthrough we are going to use the following

  • OutSystems (of course) — here we expose a sample REST API endpoint and perform a custom authentication taking the access token sent with every request by our client application. We are going to validate that token and try to login with a corresponding OutSystems user account. We will see later when and why the latter is needed.
  • Postman — We will use Postman as our client application. This is easier than to setup a sample e.g. a ReactJS application. Postman has multiple features including an integrated wizard to acquire OpenID Connect tokens.
  • AWS Cognito — In this article we are using AWS Cognito as our Identity Provider. Cognito will identify and authenticate a user and issue an access token to Postman. Feel free to use any other OAuth 2.0 / OpenID Connect capable Identity Provider (Curity, Auth0, Okta, KeyCloak, IdentityServer4 and many other commercial and open-source identity providers). The concepts will be the same, but you must figure out where to configure clients and users yourself 😒.

Identity Providers like AWS Cognito are easy to set up. That is why we are using it in this article. But they do not provide the same functionality as the more mature Identity Providers above. Functionality you will need the higher your security requirements are or the more complex your (API) environment is. Personally, i like to work with the open-source product KeyCloak.

Besides Postman and access to AWS Cognito (or any other Identity Provider) you will need the following OutSystems Forge component downloaded to your development environment.

This component by João Almeida brings everything we need to decode and validate access tokens. Leave a review and stars on Forge 👌

Accompanying this article i also published a sample service on OutSystems Forge. Please install it as well as we use this service to test and explain the steps involved.

The pattern i show in this article is the most used pattern in client to server API communication using OAuth2 / OpenID Connect. The overall flow is the following.

  • You have a client application — e.g., a ReactJS single application — which is registered with your Identity Provider.
  • A user starts the application, clicks the Login Button, and is redirected to a login page at the Identity Provider.
  • After the user successfully logged in at the Identity Provider, the user is redirected back to the application and the client application acquires an access token from the Identity Provider. (The flow here is a little more complex)
  • The client application then uses the access token to fetch data from a REST API.
  • The REST API decodes the received token. It then validates one or more header information like the issuer, targeted audience, expiration and most importantly the signature of the token.
  • Optionally the API validates so called scopes — delivered with the access token — and checks if a scope matched the requested API endpoint resource. Not part of this article though.
  • If everything is valid the REST API returns the requested data.

Some additional information

  • It is only the client application that must be registered with the Identity Provider. For additional requirements it can be useful to register your REST API as Resource Server with the Identity Provider. Not covered in this article though.
  • Your Identity Provider signs the payload of an access token with a private key only known by the Identity Provider and appends that signature to the access token. You validate the signature using the corresponding public key. Most Identity Providers share their public keys by a JSON Web Key Service endpoint. AWS Cognito is doing so as we will see. If you can validate the signature you — your API — know that the payload you received is unmodified and was signed by an Identity Provider, you trust.
  • Sometimes you read that an access token is issued to a user. This is not true. The correct statement would be that a user gives consent (at the Identity Provider) that the client application can do “things” on-behalf of the user using an access token provided by the Identity Provider.
  • OpenID Connect Identity Providers issue multiple tokens to client applications (Identity Token, Access Token, Refresh Token). Only the access token is relevant for accessing API resources. It is the access token which must contain all necessary information (called claims) to identify a user (if necessary) and authorize access to exposed resources.

But let us conclude the theory part and start with how this is implemented in OutSystems. Here is what we do

  • Create an AWS Cognito User Pool, create a sample user and register Postman as a client application.
  • Configure a Postman collection to acquire an access token.
  • Configure a custom authentication flow in an exposed REST API to handle a retrieved access token.

AWS Cognito — Create User Pool

Login to AWS console and switch to Cognito service. Click on Create User Pool.

Cognito Sign-In Experience

Check the User name sign-in option along with the Allow users to sign in with a preferred user name.

This is a little bit odd in Cognito. With Cognito it is not possible to configure which attributes (claims) are part of the access token, just like we could with other Identity Providers. An access token in Cognito only includes the username claim.

If you select only Email on this screen, then Cognito generates a unique identifier — not modifiable — for the user which also becomes the username.

Later, in OutSystems we need to query the Users entity for a matching OutSystems user, so we need a value in the username claim which matches a value in a Users entity record. (We will use the email address for that)

By checking User name and Allow users to sign in with a preferred user name we can configure the username value directly and we simply put the email address there. This still allows the user to sign-in with their email address.

Cognito Password Policy

In Multi-factor authentication disable MFA requirement by selecting the No MFA option. Deselect Enable self-service account recovery. Click Next.

Cognito Self-Service

Deselect Enable self-registration and Allow Cognito to automatically send messages to verify and confirm. For this article we will not allow an unknown user to register for an account. Instead, we will provision and confirm user accounts on our own. Click Next.

Cognito Email Notification

Select Send email with Cognito (although we will not send any emails it must be configured here). Click Next.

Cognito User Pool Configuration

Enter a User pool name of your choice and a Cognito domain prefix.

The assistant here requires us to also create a client application registration (our Postman client).

Cognito Client App

Select Confidental client and enter a App client name. Make sure that Generate a client secret is selected.

We also must add a callback URL here. After successful authentication, the user is redirected back to the application. The Identity Provider checks if the requested URL is in the configured list and performs the redirection only then.

Postman provides us with a Redirect URL, but for now simply enter https://placeholder (we change that later, when we configure Postman) and click Next.

On the Review and create page review your selection then click on Create user pool.

AWS Cognito — Create a User

In the User Pool Overview click on your created User Pool.

In the Users tab click the Create User button.

Cognito Create User

Enter the Email address of your user as User name. Later, we will use that address to lookup a corresponding user in the OutSystems User Provider, so make sure that the address you enter here does already exist in OutSystems.

Set a temporary password. This is just the initial password. When the user signs-in for the first time, the password must be changed.

Click Create user.

AWS Cognito — Postman App Client Information

Select the App Integration tab and scroll down to App clients and analytics. Click on the Postman client entry we added while creating the user pool.

Under App client information toggle Show client secret. Leave the page open as we shortly need the ClientID and Client Secret when configuring Postman.

Cognitio Client application information

Postman — Create Collection

Open Postman and in the Collections tab click New -> Collection. Enter the name of your choice for this collection.

In the Collection View go to the Variables tab first and add two variables

  • ClientID — Copy the Client ID Value from the App client information of AWS Cognito to Initial value
  • ClientSecret — Copy the Client secret value from the App client information of AWS Cognito to Initial value
Postman Collection Variables

Click the Save Button then switch to the Authorization tab.

In the Authorization tab select OAuth 2.0 from the Type dropdown.

In the Configuration Options

  • Enter a name for the token. Can be anything.
  • Make sure Authorization Code is selected as Grant Type.
  • In Client ID enter the variable you created {{ClientID}}
  • In Client Secret enter the variable you created {{ClientSecret}}
  • In Auth URL enter your Cognito domain followed by /oauth2/authorize
  • In Access Token URL enter your Cognito domain followed by /oauth2/token

You will find your Cognito domain on the App Integration tab of your User Pool under Domain.

Cognito Domain Info

It follows this naming convention

https://<your subdomain>.<your region>.amazoncognito.com

The above sample screenshot translates to

  • Auth URL — https://osdemorest.auth.eu-central-1.amazoncognito.com/oauth2/authorize
  • Access Token URL — https://osdemorest.auth.eu-central-1.amazoncognito.com/oauth2/token
  • Check the checkbox Authorize using browser
  • Copy the value of Callback URL

Back in our AWS Cognito Postman client application configuration click the Edit button in the Hosted UI section.

Overwrite the https://placeholder under Allowed Callback URLs with the copied value. Then click Save changes.

Your Authorization configuration in Postman should now look like this.

Postman Collection Authorization Configuration

Save the Collection.

Click the Get New Access Token button. Postman opens a browser window, and you get redirected to the default AWS Cognito sign-in page. Use the user you created earlier and sign-in. At the end you will be asked to open Postman. Postman will display the retrieved token. Click the Use Token button to save it.

The stores the token in Postman and is used with all REST calls associated with this collection. By default, the last retrieved token will always be used.

OutSystems — Sample Service Configuration

If you haven’t already downloaded the sample service from OutSystems Forge, please do now. Before we can give it a try you must configure some site properties of the module. These are

  • IDPIssuer — This site property holds an URI. It is used to verify the issuer (iss) claim of an access token.
  • IDPJWKS — The endpoint where public keys for signature validation can be retrieved.

You will need your AWS User Pool ID to build the URIs. This one can be found in the overview page of your User Pool.

User Pool Overview

In OutSystems Service Center open the module OpenIDRESTAPI of the sample service and configure both site properties.

IDPIssuer
https://cognito-idp.<region>.amazonaws.com/<user pool id>
IDPJWKS
https://cognito-idp.<region>.amazonaws.com/<user pool id>/.well-known/jwks.json

Postman — Query the sample service

In Postman add a new Request to your collection and give it a name e.g. Get My Profile.

Check that the Method is set to GET.

As endpoint URL enter

https://<your OutSystems environment>/OpenIDRESTAPI/rest/Profile/Me

Click the Save button.

Try to query the service using the Send button. If the Token you requested when we configured the Collection Authorization is still valid, you should see the following response

{
"Name": <Name of the user as stored in OutSystems User Provider>,
"Email": <Email address of user as stored in OutSystems User Provider>
}

If you get an error, first request a new token from the Authorization tab of your collection, then try again.

OutSystems — Sample service walkthrough

The sample service exposes a single endpoint (Me). There the Users entity is queried with a filter set to the GetUserId() function.

The service itself is set to Custom Authentication. In the OnAuthentication flow, the following is done.

  • We retrieve the Authorization header using the GetRequestHeader action from the HTTPRequestHandler module.
  • The header contains a value of Bearer followed by the actual token. We split that value to have the word Bearer and the token separated using String_Split from the Text module.
Custom Authentication
  • Using ReadTokenWithoutValidation from the JWT Module we decode the token. This is necessary so we get the information which private key AWS Cognito used to sign the token payload.
  • We query the JWKS endpoint with the extracted Key Id to retrieve the public key using GetJwkFromJwksEndpoint server action from the JWT module.
  • And then we use the ReadAndValidateToken server action from JWT module to validate the token including Expiration of token, Issuer and Signature.
Custom Authentication
  • Lastly, we filter the access token claims for the username claim with the ListFilter server action. We try to find an existing OutSystems user using an aggregate and, if found, we perform a non-persistent login of the user id using the Login server action from the System module.
Custom Authentication

Whenever there is an error or if the validation failed, we set a result status code of 403 (Forbidden) and exit by raising an exception.

And that's it. The steps in the custom authentication flow in summary:

  • Retrieve the access token from the Authorization header
  • Decode token to get the public key id
  • Download the public key from the JWKS endpoint
  • Validate token
  • Lookup OutSystems user and perform login

Notes

As you may have already discovered yourself, this flow is not optimized. I tried to show the steps involved in the simplest way possible. Although it is complex enough. Especially if you haven't worked with OAuth2 / OpenID Connect before.

It makes sense to synchronize the JWKS endpoint signature public keys on a schedule and use the cached values for token validation. This reduces the latency a little bit.

Also, a token cache can make sense, where you store the token signature, its expiry and the corresponding OutSystems user id in an entity. Whenever you retrieve a token, you look up the cached entry comparing the signature and directly login the user. This also reduces the flow a little bit.

This sample shows how to validate a token only at the backend using a public key from an issuer you trust. This is sufficient for most use cases. There is another approach called Token Introspection, where you send the token to the Identity Provider for validation. This enables other use cases like token revocation. If you have that requirement, you need a more mature Identity Provider, as AWS Cognito has not implemented the Token Introspection OAuth2 specification.

And there is of course so much more….

Thank you for reading. I hope you liked it and that i have explained the important parts well. Let me know if not 😊

If you have difficulties in getting up and running, please use the OutSystems Forum to get help. Suggestions on how to improve this article are very welcome. Send me a message via my OutSystems Profile or respond directly here on medium.

If you like my articles, please leave some claps. Follow me and subscribe to receive a notification whenever i publish a new article. Happy Low Coding!

--

--

Digital Craftsman, OutSytems MVP, AWS Community Builder and Senior Director at Telelink Business Services