OIDC applications
The Maverics Identity Orchestrator can be used as an IDP to protect OIDC apps.
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.
Grant Types
grantTypes
are methods through which client applications may obtain
tokens through particular authentication flows. If unset, the OIDC app is enabled
with authorization_code
, client_credentials
and refresh_token
.
See OIDC Provider Grant Types for more info on supported grant types.
Authentication Redirect URLs
redirectURLs
are a list of the allowed URLs the response of an authentication
request can be sent.
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
standard claims on the ID token. The claims added to the ID token are determined by
the value of the scope query 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.
Returning a nonce provided in the authorization request via a claim in the ID token response is supported as well.
For example, if email
and profile
scopes are requested in the authorization
request, the IDP being used is azure
, and the claims mapping contains mappings to
Azure attributes, then the email
and profile
claims will be included in the ID
token response with the value of the associated attribute.
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 extension
backchannel.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.
authorization
is omitted, it defaults to
allowAll: 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.
And
and
defines a list of conditions that must all be true.
Or
or
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).
IsAuthorized Service Extension
isAuthorizedSE
is an optional service extension that can be used to override the
default behavior that determines if a user is authorized.
Access Token
accessToken
defines the configuration for the OAuth access token.
Type
type
can 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.
grantTypes
are defined, refresh_token
must be specified for refresh tokens to
be issued. See Grant Types for more info.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.
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.
BuildAccessTokenClaims service extension
The buildAccessTokenClaimsSE
is an optional service extension that can customize
how claims in the access token are built.
Examples
Basic OIDC App Config Example
apps:
- name: exampleOIDCApp
type: oidc
clientID: exampleClientID
credentials:
secrets:
- <exampleClientSecretA>
- <exampleClientSecretB>
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
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:
rules:
- and:
- contains: [ "{{ ldap.groups }}", "admin" ]
- contains: ["{{ okta.email }}", "@example.com"]
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
}