The Maverics Identity Orchestrator can be used as an IDP to protect OIDC apps.
To define OIDC type apps the oidcProvider must be defined.
Configuration options
Name
name is a unique identifier for the app.
Type
type represents the application type. When defining OIDC apps, the type should be oidc.
Client ID
clientID is a unique ID used by client applications to identify themselves.
Credentials
credentials encapsulates the configuration which specifies how the application validates its client credentials.
Secrets
secrets is an array which specifies multiple secret values which can be used for authenticating client applications. The value of the secrets can be stored in a secret provider.
Demonstrating Proof of Possession (DPoP)
dpop is a mechanism for sender-constraining OAuth 2.0 tokens via proof-of-possession. See RFC 9449 to learn more about DPoP.
Please note that DPoP proofs will be rejected if they are older than five minutes, or issued more than 5 minutes in the future. That is, a given proof must not have an issued at claim (iat) more than five minutes in the past or future.
When enabled, DPoP proofs are required to include a nonce claim, per this section of RFC 9449. The nonce ensures the maximum age of the DPoP proof and prevents an attacker from minting DPoP proofs in the future. Orchestrator clients should expect a 400 response with the error use_dpop_nonce when the nonce is missing from the DPoP proof. The current nonce is obtained by reading the DPoP-Nonce HTTP header in the same 400 response.
Enabled
enabled is an optional boolean field that determines whether DPoP will be required
for the application.
Nonce
Authorization server-provided nonces are used to limit the lifetime of DPoP proofs. Nonces are required by default.
Disabled
Nonces can be optionally disabled, but doing so is NOT recommended. Details on the security vulnerabilities of disabling nonces can be found here.
Grant Types
grantTypes are a list of allowed methods through which client applications may obtain tokens via a particular authentication flow. If unset, the OIDC app is enabled with the authorization_code, client_credentials and refresh_token grants by default.
The list of available grants include:
authorization_codeclient_credentialsrefresh_tokenpasswordimplicit_idimplicit_token
The
implicit_idandimplicit_tokengrant types are used for supporting the Implicit Flow. When theimplicit_idandimplict_tokengrants are defined, an ID token and access token are respectively returned from the authorization endpoint.Please note that the
implicit_idgrant can be combined withauthorization_codegrant to implement the Hybrid Flow.
Public
public is an optional boolean field that determines whether the OIDC app is a public client. Public clients, such as SPAs and native applications, cannot securely store authentication credentials. Therefore, public clients support a limited set of grant types, namely:
authorization_coderefresh_tokenimplicit_idimplicit_token
When using the
authorization_codegrant, public clients must use PKCE.Public clients cannot use flows that require a client secret, such as
client_credentialsorpassword.
Insecure Skip PKCE
insecureSkipPKCE is an optional boolean field that determines whether public clients can skip using PKCE when using the authorization_code grant type.
Per OAuth 2.0 Security Best Current Practice, public clients MUST use PKCE when using the
authorization_codegrant type. TheinsecureSkipPKCEoption should only be used for legacy apps that are unable to use PKCE. Avoid using this configuration unless absolutely necessary.
Authentication Redirect URLs
redirectURLs are a list of the allowed URLs the response of an authentication request can be sent. Any query parameters defined in a redirect URL must also be present and match in the authentication request.
Default Redirect URL
allowDefaultRedirectURI is an optional boolean flag that determines whether clients can make authorization requests without providing the redirect_uri parameter. If set to true, the OIDC app will use the first URL in the redirectURLs list as the default redirect URI. The default redirect URI will not override the redirect_uri parameter if it is provided in the authorization request.
Per the OIDC RFC, the
redirect_uriis a required parameter. TheallowDefaultRedirectURIoption should only be used for apps that are unable to provide a redirect URI. Avoid using this configuration unless absolutely necessary.
Logout Redirect URLs
logoutRedirectURLs are a list of the allowed URLs the response of a logout request can be sent.
Claims Mapping
claimsMapping provides a way to map attributes on a user's session to claims on the ID token. The claims added to the ID token are determined by the value of the scope parameter sent in the authorization request. The scopes supported, in addition to the required openid scope are email, profile, address and phone. The mapping of claim values to a scope is listed in OpenID Connect Core 1.0, section 5.4.
For example, consider an authorization request where the openid and email scopes are requested. Assuming a correct claimsMapping, the ID token would include a sub claim, email claim, and email_verified claim.
Returning a nonce provided in the authorization request via a claim in the ID token response is supported as well.
Attribute Providers
attrProviders are an optional configuration for an identity system or data store from which the OIDCProvider may retrieve additional attributes used in claimsMapping.
Connector
connector is a reference to the name of the defined connector which will be used as an attribute provider.
Username Mapping
usernameMapping defines the attribute that will be used as a search key to query for the user's attributes.
Authentication
authentication defines how users are authenticated for this app.
IDPs
idps lists the IDPs which will be used to authenticate the user.
IsAuthenticated service extension
isAuthenticatedSE is an optional service extension that can be used to override the default behavior that determines if a user is already authenticated. This extension must be used with authenticateSE.
Authenticate service extension
authenticateSE is an optional service extension used to take control of how authentication will be done. This extension must be used with isAuthenticatedSE.
Backchannel
Authenticate service extensionbackchannel.authenticateSE is an optional service extension used to take control of how end-user authentication will be done for backchannel OIDC flows such as Resource Owner Password Credentials Grant.
See Grant Types for more information on enabling password flow.
Load Attributes Service Extension
loadAttrsSE is an optional service extension used to customize how the loading of attributes is done. This extension is often used to load attributes from proprietary data sources such as enterprise APIs.
Authorization
authorization is an optional config which defines access control rules that
are required to access the app.
If
authorizationis omitted, it defaults toallowAll: true. All authenticated users will be authorized to access the app.
Allow All
allowAll enables open access to a given app. This option is usually used when fine-grained authorization is not being enforced.
Rules
rules define a list of access control conditions. All rules must evaluate to true in order user to get access to a given app.
Andand defines a list of conditions that must all be true.
Oror defines a list of conditions where at least one condition must be true.
Operators
The operators defined below can be listed under an or or and rule. All operators expect exactly two values (operands).
In order to load an attribute from the session cache that will be leveraged in policy, use the {{ connectorName.attributeName }} syntax. For example, {{ ldap.memberOf }} could be used to write a policy that was predicated on LDAP group membership.
Additionally, an HTTP request method can be considered during policy evaluation by using the {{ http.request.method }} syntax. The HTTP request method will be returned as an uppercase string, for example, POST. Please note that only the HTTP request method attribute can be used in policy, but support for other HTTP attributes will be added over time.
equals evaluates to true if the two values are equivalent.
notEquals evaluates to true if the two values are not equivalent.contains evaluates to true if the left-hand operand (the complete string) contains the right-hand operand (a substring).
notContains evaluates to true if the left-hand operand (the complete string) does not contain the right-hand operand (a substring).
Rules Aggregation Method
rulesAggregationMethod defines how the rules are aggregated. The default aggregation method is a logical and, meaning that all rules must evaluate to true in order for a user to be authorized. When set to or, at least one rule must evaluate to true in order for a user to be authorized.
IsAuthorized Service Extension
isAuthorizedSE is an optional service extension that can be used on its own or in addition to rules to determine if a user is authorized. When both rules and isAuthorizedSE are configured, a user will be authorized if both the rules and the isAuthorizedSE evaluate to true.
Access Token
accessToken defines the configuration for the OAuth access token.
Type
typecan be set to either jwt (default) or opaque.
Length
length defines the length of an opaque access token. The length can set to between 22 and 256 characters. If unset, the default length is 28 characters.
Lifetime Seconds
lifetimeSeconds controls the lifetime of an access token. By default, access tokens have a lifetime of one hour.
Refresh Token
refreshToken defines the configuration for the OAuth refresh token. Refresh tokens are used to get a new access token without user interaction.
Refresh tokens are automatically rotated with every access token refresh. That is, everytime a refresh token is used to generate a new access token, a new refresh token is also returned. If a previously issued refresh token is reused outside an acceptable window, the active refresh token will be invalidated.
Refresh tokens are rotated to protect against refresh token replay attacks and compromised long-lived refresh tokens. For more information, please reference the OAuth Security Topics RFC.
Allow Offline Access
allowOfflineAccess defines whether a client can request refresh tokens. Refresh tokens will only be issued if the allowOfflineAccess flag is set to true and the authorization request includes the offline_access scope.
If
grantTypesare defined,refresh_tokenmust be specified for refresh tokens to be issued. See Grant Types for more info.
Lifetime Seconds
lifetimeSeconds controls the lifetime of a refresh token. By default, refresh tokens have a lifetime of 24 hours.
Length
length can be set to between 22 and 256 characters to define the length of a refresh token. If unset, the default length is 28 characters.
CORS
cors is an optional configuration that defines the CORS policy for the OIDC app.
Allowed Origins
allowedOrigins is a list of allowed origins that can make cross-origin requests to the OIDC provider. Browser clients will use this configuration to determine if the OIDC provider is allowed to respond to cross origin requests. The value of allowedOrigins must be a valid URL, such as https://app.enterprise.com.
For production environments, it is recommended to only use https URLs in the
allowedOriginslist. Non-secure origins (http) can lead to security vulnerabilities, such as cross-site scripting (XSS) attacks.
Allowed Credentials
allowedCredentials is a boolean flag that controls whether the OIDC Provider includes the HTTP response header: Access-Control-Allow-Credentials: true.
When set to true, browsers are permitted to expose responses to requests that include credentials such as cookies, authorization headers, or TLS client certificates.
Enabling
allowedCredentialsbroadens what data browsers will expose to client code. Only set it for trusted origins that you control.
AllowedAudiences
allowedAudiences is an optional configuration that represents a list of audiences that are allowed to consume access tokens. When a client makes a request to the authorization endpoint, an optional resource parameter can be included that indicates the target audience of the token. The value provided in the resource parameter must be on the list of allowed audiences.
This configuration is used when resource servers, such as APIs, authorize via the access token. For more information, please reference RFC 9068.
BuildIDTokenClaims service extension
The buildIDTokenClaimsSE is an optional service extension that can customize how claims in the ID token are built. This service extension should be used when non-standard claims need to be added to the ID token.
BuildAccessTokenClaims service extension
The buildAccessTokenClaimsSE is an optional service extension that can customize how claims in the access token are built. This service extension should be used when non-standard claims need to be added to the access token.
Custom Scopes
customScopes is an optional config used to specify non-standard OIDC scopes available to a client application.
Scopes
scopes is a list of custom scopes.
Name
name is the string literal of the custom scope, e.g. read:user.
Examples
Basic OIDC App Config Example
apps:
- name: exampleOIDCApp
type: oidc
clientID: exampleClientID
credentials:
secrets:
- <exampleClientSecretA>
- <exampleClientSecretB>
refreshToken:
allowOfflineAccess: true
lifetimeSeconds: 7200
accessToken:
type: jwt
dpop:
enabled: true
nonce:
disabled: false
redirectURLs:
- https://app.enterprise.com/oidc
logoutRedirectURLs:
- https://app.enterprise.com/oidc/logout
attrProviders:
- connector: ldap
usernameMapping: okta.email
authentication:
idps:
- okta
claimsMapping:
email: ldap.mail
given_name: ldap.givenname
family_name: ldap.sn
customScopes:
scopes:
- name: read:user
OIDC App Config Example With CORS Example
apps:
- name: exampleOIDCApp
type: oidc
clientID: exampleClientID
credentials:
secrets:
- <exampleClientSecretA>
- <exampleClientSecretB>
refreshToken:
allowOfflineAccess: true
accessToken:
type: jwt
dpop:
enabled: true
nonce:
disabled: false
redirectURLs:
- https://app.enterprise.com/oidc
logoutRedirectURLs:
- https://app.enterprise.com/oidc/logout
attrProviders:
- connector: ldap
usernameMapping: okta.email
authentication:
idps:
- okta
claimsMapping:
email: ldap.mail
given_name: ldap.givenname
family_name: ldap.sn
cors:
allowedOrigins:
- https://app.enterprise.com
allowedCredentials: true
OIDC App Config Example With Authorization Rules
apps:
- name: exampleOIDCApp
type: oidc
clientID: exampleClientID
credentials:
secrets:
- <exampleClientSecret>
redirectURLs:
- https://app.enterprise.com/oidc
logoutRedirectURLs:
- https://app.enterprise.com/oidc/logout
attrProviders:
- connector: ldap
usernameMapping: okta.email
authentication:
idps:
- okta
authorization:
rulesAggregationMethod: or
rules:
- and:
- contains: [ "{{ ldap.groups }}", "admin" ]
- contains: ["{{ okta.email }}", "@example.com"]
- and:
- contains: [ "{{ ldap.groups }}", "executive" ]
- contains: ["{{ okta.email }}", "@example.com"]
isAuthorizedSE:
funcName: IsAuthorized
file: /etc/maverics/extensions/auth.go
OIDC App With Service Extensions
apps:
- name: exampleOIDCApp
type: oidc
clientID: exampleClientID
credentials:
secrets:
- <exampleClientSecret>
grantTypes:
- authorization_code
- password
redirectURLs:
- https://app.enterprise.com/oidc
authentication:
isAuthenticatedSE:
funcName: IsAuthenticated
file: /etc/maverics/extensions/auth.go
authenticateSE:
funcName: Authenticate
file: /etc/maverics/extensions/auth.go
backchannel:
authenticateSE:
funcName: BackchannelAuthenticate
file: /etc/maverics/extensions/auth.go
loadAttrsSE:
funcName: LoadAttributes
file: /etc/maverics/extensions/auth.go
authorization:
isAuthorizedSE:
funcName: IsAuthorized
file: /etc/maverics/auth.go
buildAccessTokenClaimsSE:
funcName: BuildAccessTokenClaims
file: /etc/maverics/extensions/auth.go
buildIDTokenClaimsSE:
funcName: BuildIDTokenClaims
file: /etc/maverics/extensions/auth.go
claimsMapping:
email: okta.email
/etc/maverics/extensions/auth.go
package main
import (
"net/http"
"errors"
"crypto/sha256"
"encoding/hex"
"github.com/strata-io/service-extension/orchestrator"
)
func IsAuthenticated(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) bool {
logger := api.Logger()
logger.Debug("se", "determining if user is authenticated")
session, err := api.Session()
if err != nil {
logger.Error("se", "unable to retrieve session", "error", err.Error())
return false
}
isOktaAuth, err := session.GetString("okta.authenticated")
if err != nil {
logger.Error("se", "unable to retrieve session value", "error", err.Error())
return false
}
if isOktaAuth == "true" {
return true
}
return false
}
func Authenticate(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) {
logger := api.Logger()
logger.Debug("se", "authenticating user")
oktaIDP, err := api.IdentityProvider("okta")
if err != nil {
logger.Error(
"se", "failed to retrieve Okta IDP",
"error", err.Error(),
)
http.Error(
rw,
http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError,
)
return
}
oktaIDP.Login(rw, req)
}
func LoadAttributes(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) error {
logger := api.Logger()
logger.Debug("se", "loading custom attributes from LDAP")
session, err := api.Session()
if err != nil {
logger.Error("se", "unable to retrieve session", "error", err.Error())
return err
}
mail, err := session.GetString("okta.email")
if err != nil {
return fmt.Errorf("failed to find user email required for LDAP query: %w", err)
}
ldap, err := api.AttributeProvider("ldap")
if err != nil {
return fmt.Errorf("failed to find LDAP attribute provider")
}
attrs, err := ldap.Query(mail, []string{"givenname", "sn", "mobile"})
if err != nil {
return fmt.Errorf("failed to query LDAP: %w", err)
}
for k, v := range attrs {
logger.Debug(
"se", "setting LDAP attribute on session",
"attribute", k,
"value", v,
)
_ = session.SetString(k, v)
}
err = session.Save()
if err != nil {
return fmt.Errorf("unable to save session state: %w", err)
}
return nil
}
func BackchannelAuthenticate(api orchestrator.Orchestrator, req *http.Request) error {
logger := api.Logger()
logger.Debug("se", "authenticating user in backchannel flow")
username := req.Form.Get("username")
password := req.Form.Get("password")
secrets, err := api.SecretProvider()
if err != nil {
logger.Error(
"se", "failed to retrieve secret provider",
"error", err.Error(),
)
return errors.New("failed to retrieve secret provider")
}
pwdHash := secrets.GetString(username)
if len(pwdHash) == 0 {
return errors.New("invalid user credentials")
}
hash := sha256.Sum256([]byte(password))
hashHex := hex.EncodeToString(hash[:])
if pwdHash != hashHex {
return errors.New("invalid user credentials")
}
return nil
}
func IsAuthorized(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) bool {
logger := api.Logger()
logger.Debug("se", "determining if user is authorized")
session, err := api.Session()
if err != nil {
logger.Error("se", "unable to retrieve session", "error", err.Error())
return false
}
rawGroups, _ := session.GetString("okta.groups")
groups := strings.Split(rawGroups, ",")
for _, group := range groups {
if group == "executives" {
return true
}
}
return false
}
func BuildAccessTokenClaims(api orchestrator.Orchestrator, _ *http.Request) (map[string]any, error) {
logger := api.Logger()
logger.Debug("se", "building access token claims")
session, err := api.Session()
if err != nil {
logger.Error("se", "unable to retrieve session", "error", err.Error())
return nil, err
}
roles, err := session.GetString("okta.roles")
return map[string]any{
"scope": roles,
}, err
}
func BuildIDTokenClaims(api orchestrator.Orchestrator, _ *http.Request) (map[string]any, error) {
logger := api.Logger()
logger.Debug("se", "building ID token claims")
session, err := api.Session()
if err != nil {
logger.Error("se", "unable to retrieve session", "error", err.Error())
return nil, err
}
groups, err := session.GetString("okta.groups")
return map[string]any{
"groups": groups,
}, err
}