How it works
Use cases
Goiabada is useful in two main scenarios:
- When users need access to specific resources (such as a section of your application or an API) and you want to manage that access.
- When servers need to access other servers, and you want to set defined permission levels for them.
Let's view those in more details.
Users accessing resources
When you have users accessing resources, you basically need to know: who the user is (authentication), and whether they're authorized to access that resource (authorization).
Goiabada works with two familiar web protocols to fulfil that: OpenID Connect handles the who's who (authentication), and OAuth2 takes care of who can do what (authorization).
Regardless of your app type (a web app on the server side, a web app using JavaScript, or a mobile native app), the recommended approach is the Authorization code flow with PKCE.
The Authorization code flow with PKCE is a secure method for handling user authentication in web applications. It works in two steps: first, the application requests an authorization code from the /authorize
endpoint. Then, it exchanges this code for an access token, a refresh token, and optionally an ID token at the /token
endpoint.
PKCE adds an extra layer of security by preventing interception of the authorization code, especially in public clients like mobile or single-page applications.
Server to server communications
When you have a set of servers working together, and you want to ensure that only the right clients can access resources on a specific server, go for the Client credentials flow, with a confidential client.
Learn more about OAuth2
OAuth2 covers a lot of ground. To delve deeper into it, check out this link - https://www.oauth.com/
Clients
A client represents an application that requests access to protected resources.
This access can either be on behalf of a user (using the authorization code flow with PKCE) or for the client itself (using the client credentials flow).
Public or confidential clients
Clients can be either public or confidential.
A public client is recommended for applications that cannot ensure the confidentiality of their client credentials. This is relevant for JavaScript-only web applications, where any secrets are exposed in the browser, and also in mobile apps, where an APK can be decompiled, revealing stored secrets.
A confidential client is recommended for applications that can securely protect client credentials, such as server-side applications. Confidential clients can safely store sensitive information like passwords, as they run on a secure server rather than on a user's device or browser.
Consent required
In OAuth2, the consent process is important for ensuring that users explicitly authorize third-party applications to access their resources.
When the client is affiliated with the same organization as the authorization server and a high level of trust exists, explicit consent is not usually required.
However, for clients from third-party organizations, it's important to configure the client to request user consent. This ensures that users are aware of who is accessing their tokens.
Default ACR level
ACR stands for "Authentication Context Class Reference." It's a way to specify the level of authentication assurance or the strength of the authentication method used to authenticate the end-user.
Goiabada has 3 levels:
ACR level | Description |
---|---|
urn:goiabada:level1 |
Level 1 authentication only (password) |
urn:goiabada:level2_optional |
Level 1 with optional 2fa (if 2fa is enabled by the user) |
urn:goiabada:level2_mandatory |
Level 1 with mandatory 2fa |
By default, a client comes configured with urn:goiabada:level2_optional
.
You have the flexibility to override the client's default ACR level on a per-authorization basis. For example, if your client has the default urn:goiabada:level2_optional
but you have a specific resource that requires users to authenticate using two-factor authentication (2fa), you can specify urn:goiabada:level2_mandatory
in the acr_values
parameter of the authorization request.
Redirect URIs
In the Authorization code flow with PKCE, the client application specifies a redirect URI in its authorization request.
After the user grants or denies permission, the authorization server redirects the user back to this specified URI.
It's necessary to pre-configure this URI in the client, and only exact matches are accepted (no wildcards).
Web origins
If your client application plans to make calls to the /token
, /logout
or /userinfo
endpoints from Javascript, you must register the URL (origin) of the web application here, to enable Cross-Origin Resource Sharing (CORS) access. Failure to do so will result in CORS blocking the HTTP requests.
Client permissions
Client permissions are used in server-to-server checks, specifically within the client credentials flow. This is about the permissions granted to the client itself, allowing it to access other resources.
Resources and permissions
In Goiabada, you have the ability to define both resources and permissions. Each resource can have multiple permissions associated with it.
You can assign these permissions to users, groups, or clients as needed.
Scope
When you pair a resource with a permission, it forms a scope, both in the authorization request and within the tokens. For example, if you have a resource identified as product-api
and a permission identified as delete-product
the corresponding scope will be represented as product-api:delete-product
.
OpenID Connect scopes
Besides the authorization scopes that are formed by resources and permissions (as explained in the previous section), Goiabada supports typical OpenID Connect scopes. They are:
OIDC scope | Description |
---|---|
openid | Will include an id_token in the token response, with the subject identifier (sub claim) |
profile | Access to claims: name , family_name , given_name , middle_name , nickname , preferred_username , profile , website , gender , birthdate , zoneinfo , locale , and updated_at |
Access to claims: email , email_verified |
|
address | Access to the address claim |
phone | Access to claims: phone_number and phone_number_verified |
groups | Access to the list of groups the user belongs to |
attributes | Access to the attributes assigned to the user by an admin, stored as key-value pairs |
offline_access | Access to a refresh token of the type Offline , allowing the client to obtain a new access token without requiring an immediate interaction |
User sessions
User sessions facilitate the single sign-on (SSO) functionality of Goiabada. Once a user logs in, a new session starts. If they try to log in again and their session is still good, they don't need to go through the authentication process again.
There are two configurations that are related to the user session:
Property | Description |
---|---|
User session idle timeout in seconds | If there is no activity from the user within this timeframe, the session will be terminated. This will look into the last_accessed timestamp of the session. |
User session max lifetime in seconds | The maximum duration a user session can last, irrespective of user activity. This will be checked against the started timestamp of the session. |
A user session is bumped (which means, gets a new last_accessed
timestamp) in two situations:
- When a new authorization request completes
- When a refresh token associated with the session is used to request a new access token
In your authorization request, you have the option to include the max_age
parameter. This parameter allows you to define the maximum acceptable time (in seconds) since the user's last authentication. For instance, if you add max_age=120
to the authentication request, it implies that the user needs to re-authenticate if their last authentication was over 120 seconds (2 minutes) ago, regardless of having a valid session. This is useful when the client needs to ensure that the user authenticated within a specific timeframe.
Token expiration
You can customize the expiration (in seconds) for access tokens and id tokens on the Settings → Tokens page. These configurations apply globally to all clients. However, if needed, individual clients have the flexibility to override the global settings in their specific client configurations.
The default token expiration is set to 5 minutes. Access tokens are intentionally kept short-lived, for security reasons.
Refresh tokens
Refresh tokens are used in the authorization code flow with PKCE (in the client credentials flow we don't have refresh tokens).
Goiabada supports two types of refresh tokens: normal and offline.
Normal tokens are linked to the user session. They can be used to get a new access token, as long as there's an active user session. When a normal refresh token is used, the user session last_accessed
timestamp is bumped. The expiration time of a normal refresh token is the same as the user session idle timeout (default is 2 hours). If the user session is terminated, it will automatically invalidate the refresh tokens linked to that session.
Offline refresh tokens are not linked to a user session. They can be used to obtain a new access token even when the user is not actively using the application. Their expiration time is long (defaults to 30 days).
In your authorization request, when you ask for the offline_access
scope, your refresh token will be classified as offline
. Otherwise, if you don't include the offline_access
scope, your refresh token will be considered normal.
Upon each usage of a refresh token, the refresh token passed in to the /auth/token
endpoint becomes inactive, and a new refresh token is provided in the token response. In other words, a refresh token is a one-time-use token; once used, it must be substituted with the new refresh token obtained from the response.
Users and groups
As an administrator of Goiabada you can create users and configure their properties (profile information, address, phone, email...). Also, you have the capability to modify their credentials, terminate active user sessions, and revoke consents.
You can also assign permissions and attributes to individual users. Attributes are key-value pairs or arbitraty information, and can be included in the access token or id token.
To facilitate user management, you can create groups of users. When you give a permission to a group, it's given to all group members. The same applies to attributes - group attributes will be included for all group members.
Attributes are arbitrary key-value pairs that you can associate with either a user or a group. When creating an attribute, you can choose to include it either in the access token or the id token.
Self registration
When the 'Self registration' setting is activated, users gain the ability to independently register their accounts using a link incorporated into the login form. If this setting is disabled, only administrators have the privilege of creating new user accounts.
For self-registrations, there's an option to require email verification for new users. Enabling this ensures that an account only becomes active after the user clicks a verification link sent to their email. To use this feature, be sure to configure your SMTP settings.
Endpoints
Well-known discovery URL
You can find a link to the well-known discovery URL by going to the root of the admin console. The URL will look like this:
https://demo-authserver.goiabada.dev/.well-known/openid-configuration
This endpoint will show the capabilities that are supported by Goiabada.
/auth/authorize (GET)
The authorize endpoint is used to request authorization codes via the browser. This process normally involves authentication of the end-user and giving consent, when required.
Parameters (* are mandatory):
Parameter | Description |
---|---|
client_id | The client identifier. |
redirect_uri | The redirect URI is the callback entry point of the app. In other words, it's the location where the authorization server sends the user once the /auth/authorize process completes. This must exactly match one of the allowed redirect URIs for the client. |
response_type | code is the only value supported - for the authorization code flow with PKCE. |
code_challenge_method | S256 is the only value supported - for a SHA256 hash of the code verifier. |
code_challenge | A random string between 43 and 128 characters long. |
response_mode | Supported values: query , fragment or form_post . With query the authorization response parameters are encoded in the query string of the redirect_uri . With fragment they are encoded in the fragment (#). And form_post will make the parameters be encoded as HTML form values that are auto-submitted in the browser, via HTTP POST. |
max_age | If the user's authentication timestamp exceeds the max age (in seconds), they will have to re-authenticate |
acr_values | Supported values are: urn:goiabada:pwd , urn:goiabada:pwd:otp_ifpossible or urn:goiabada:pwd:otp_mandatory . This will override the default ACR level configured in the client for this authorization request. See Default ACR level. |
state | Any string. Goiabada will echo back the state value on the token response, for CSRF/replay protection. |
nonce | Any string. Goiabada will echo back the nonce value in the identity token, as a claim, for replay protection. |
scope | One or more registered scopes, separated by a space character. A registered scope can be either a resource:permission or an OIDC scope. See Scope and OpenID Connect scopes. |
/auth/token (POST)
The token endpoint serves the purpose of requesting tokens. This can happen either through the authorization code flow, involving the exchange of an authorization code for tokens, or through the client credentials flow, where a client directly requests tokens.
Parameters:
Parameter | Description |
---|---|
grant_type | Supported grant types are authorization_code (to exchange an authorization code for tokens), client_credentials (for the client credentials flow) or refresh_token (to use a refresh token). |
client_id | The client identifier. |
client_secret | The client secret, if it's a confidential client. |
redirect_uri | Required for the authorization_code grant type. |
code | The authorization code. Required for the authorization_code grant type. |
code_verifier | This is the code verifier associated with the PKCE request, initially generated by the app before the authorization request. It represents the original string from which the code_challenge was derived. |
scope | This parameter is used in the client_credentials and refresh_token grant types. In client_credentials grant type, it's a mandatory parameter, and it should encompass one or more registered scopes, separated by a space character. These scopes represent the requested permissions in the format of resource:permission . For the refresh_token grant type, the scope parameter is optional and serves to restrict the original scope to a more specific and narrower subset. |
refresh_token | The refresh token, required for the refresh_token grant type. |
/auth/logout (GET or POST)
This endpoint enables the client application to initiate a logout. The client application calls this logout endpoint on the auth server. Upon successful logout from the auth server, the user agent is then redirected to a logout link within the client application. This implementation aligns with the OpenID Connect RP-Initiated Logout 1.0 protocol.
If the /auth/logout
endpoint is invoked without parameters, it will display a logout consent screen, prompting the user to confirm their intention to log out. There will be no redirection to the client application in this scenario.
The recommended way of calling /auth/logout
involves including additional parameters:
Parameter | Description |
---|---|
id_token_hint | The previously issued id token. |
post_logout_redirect_uri | A post-logout URI, which must be pre-registered with the client as a redirect URI. Once the logout is finalized on the authentication server, the user agent will be redirected to this post-logout URI. This allows for the termination of the session on the client application as well. |
client_id | The client identifier. Mandatory if the id_token_hint parameter is encrypted with the client secret. |
state | Any arbitraty string that will be echoed back in the post_logout_redirect_uri . |
The two possible routes are:
id_token_hint
(unencrypted) +post_logout_redirect_uri
+state
(optional).id_token_hint
(encrypted with AES GCM) +post_logout_redirect_uri
+client_id
+state
(optional).
Encrypting the id_token_hint
(option 2) enhances security by preventing the exposure of the ID token on the client side. Without encryption, calling this endpoint with an unencrypted id_token_hint
could potentially expose personally identifiable information (PII) and other claims that are inside of the id token, such as the client identifier.
Below are some examples on how to encrypt the id token for the id_token_hint
parameter. You must URL-encode the resulting base64 string, when sending it as a querystring parameter to /auth/logout
.
.NET C#
private static string AesGcmEncryption(string idTokenUnencrypted,
string clientSecret)
{
var key = new byte[32];
// use the first 32 bytes of the client secret as key
var keyBytes = Encoding.UTF8.GetBytes(clientSecret);
Array.Copy(keyBytes, key, Math.Min(keyBytes.Length, key.Length));
// random nonce
var nonce = new byte[AesGcm.NonceByteSizes.MaxSize]; // MaxSize = 12
RandomNumberGenerator.Fill(nonce);
using var aes = new AesGcm(key);
var cipherText = new byte[idTokenUnencrypted.Length];
var tag = new byte[AesGcm.TagByteSizes.MaxSize]; // MaxSize = 16
aes.Encrypt(nonce, Encoding.UTF8.GetBytes(idTokenUnencrypted),
cipherText, tag);
// concatenate nonce (12 bytes) + ciphertext (? bytes) + tag (16 bytes)
var encrypted = new byte[nonce.Length + cipherText.Length + tag.Length];
Array.Copy(nonce, encrypted, nonce.Length);
Array.Copy(cipherText, 0, encrypted, nonce.Length, cipherText.Length);
Array.Copy(tag, 0, encrypted, nonce.Length + cipherText.Length, tag.Length);
return Convert.ToBase64String(encrypted);
}
Go
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"
"math"
)
func AesGcmEncryption(idTokenUnencrypted string, clientSecret string) (string, error) {
key := make([]byte, 32)
// Use the first 32 bytes of the client secret as key
keyBytes := []byte(clientSecret)
copy(key, keyBytes[:int(math.Min(float64(len(keyBytes)), float64(len(key))))])
// Random nonce
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
aesGcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
cipherText := aesGcm.Seal(nil, nonce, []byte(idTokenUnencrypted), nil)
// Concatenate nonce (12 bytes) + ciphertext (? bytes) + tag (16 bytes)
encrypted := make([]byte, len(nonce)+len(cipherText))
copy(encrypted, nonce)
copy(encrypted[len(nonce):], cipherText)
return base64.StdEncoding.EncodeToString(encrypted), nil
}
NodeJS
const crypto = require('crypto');
function aesGcmEncryption(idTokenUnencrypted, clientSecret) {
const key = Buffer.alloc(32);
// Use the first 32 bytes of the client secret as the key
const keyBytes = Buffer.from(clientSecret, 'utf-8');
keyBytes.copy(key, 0, 0, Math.min(keyBytes.length, key.length));
// Random nonce
const nonce = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
let cipherText = cipher.update(idTokenUnencrypted, 'utf-8', 'base64');
cipherText += cipher.final('base64');
const tag = cipher.getAuthTag();
// Concatenate nonce (12 bytes) + ciphertext (? bytes) + tag (16 bytes)
const encrypted = Buffer.concat([nonce, Buffer.from(cipherText, 'base64'), tag]);
return encrypted.toString('base64');
}
You can explore the libraries available on your platform and use the same approach as shown here.
/userinfo (GET or POST)
The UserInfo endpoint, a component of OpenID Connect, is used to retrieve identity information about a user.
The caller needs to send a valid access token to be able to access this endpoint. This is done by adding the Authorization: Bearer token-value
header to the HTTP request.
The endpoint validates the presence of the authserver:userinfo
scope within the access token. If this scope is present, the endpoint responds by providing claims about the user.
Please note that you don't need to manually request the authserver:userinfo
scope in the authorization request. Instead, it will be automatically included in the access token whenever any OpenID Connect scope is included in the request.
The specific claims returned by the UserInfo endpoint depend on the OpenID Connect scopes included in the access token. For instance, if the openid
and email
scopes are present, the endpoint will return the sub
(subject) claim from the openid
scope, as well as the email
and email_verified
claims from the email scope.