The OAuth2 Filter
The OAuth2
filter type performs OAuth2 authorization against an identity provider implementing OIDC Discovery. The filter is both:
- An OAuth Client, which fetches resources from the Resource Server on the user's behalf.
- Half of a Resource Server, validating the Access Token before allowing the request through to the upstream service, which implements the other half of the Resource Server.
This is different from most OAuth implementations where the Authorization Server and the Resource Server are in the same security domain. With the Ambassador Edge Stack, the Client and the Resource Server are in the same security domain, and there is an independent Authorization Server.
The Ambassador Authentication Flow
This is what the authentication process looks like at a high level when using Ambassador Edge Stack with an external identity provider. The use case is an end-user accessing a secured app service.
Some basic authentication terms
For those unfamiliar with authentication, here is a basic set of definitions.
- OpenID: is an open standard and decentralized authentication protocol. OpenID allows users to be authenticated by co-operating sites, referred to as "relying parties" (RP) using a third-party authentication service. End-users can create accounts by selecting an OpenID identity provider (such as Auth0, Okta, etc), and then use those accounts to sign onto any website that accepts OpenID authentication.
- Open Authorization (OAuth): an open standard for token-based authentication and authorization on the Internet. OAuth provides to clients a "secure delegated access" to server or application resources on behalf of an owner, which means that although you won't manage a user's authentication credentials, you can specify what they can access within your application once they have been successfully authenticated. The current latest version of this standard is OAuth 2.0.
- Identity Provider (IdP): an entity that creates, maintains, and manages identity information for user accounts (also referred to "principals") while providing authentication services to external applications (referred to as "relying parties") within a distributed network, such as the web.
- OpenID Connect (OIDC): is an authentication layer that is built on top of OAuth 2.0, which allows applications to verify the identity of an end-user based on the authentication performed by an IdP, using a well-specified RESTful HTTP API with JSON as a data format. Typically an OIDC implementation will allow you to obtain basic profile information for a user that successfully authenticates, which in turn can be used for implementing additional security measures like Role-based Access Control (RBAC).
- JSON Web Token (JWT): is a JSON-based open standard for creating access tokens, such as those generated from an OAuth authentication. JWTs are compact, web-safe (or URL-safe), and are often used in the context of implementing single sign-on (SSO) within federated applications and organizations. Additional profile information, claims, or role-based information can be added to a JWT, and the token can be passed from the edge of an application right through the application's service call stack.
If you look back at the authentication process diagram, the function of the entities involved should now be much clearer.
Using an Identity Hub
Using an identity hub or broker allows you to support many IdPs without having to code individual integrations with them. For example, Auth0 and Keycloak both offer support for using Google and GitHub as an IdP.
An identity hub sits between your application and the IdP that authenticates your users, which not only adds a level of abstraction so that your application (and Ambassador Edge Stack) is isolated from any changes to each provider's implementation, but it also allows your users to chose which provider they use to authenticate (and you can set a default, or restrict these options).
The Auth0 docs provide a guide for adding social IdP "connections" to your Auth0 account, and the Keycloak docs provide a guide for adding social identity "brokers".
OAuth2
Global Arguments
---apiVersion: getambassador.io/v2kind: Filtermetadata:name: "example-oauth2-filter"namespace: "example-namespace"spec:OAuth2:authorizationURL: "url-string" # requiredgrantType "enum-string" # optional; default is "AuthorizationCode"extraAuthorizationParameters: # optional; default is {}"string": "string"accessTokenValidation: "enum-string" # optional; default is "auto"accessTokenJWTFilter: # optional; default is nullname: "string" # requirednamespace: "string" # optional; default is the same namespace as the Filterarguments: JWT-Filter-Arguments # optional# ClientURL is required when grantType=="AuthorizationCode", and not allowed otherwise.clientURL: "url-string"# ClientID is required for grantType "AuthorizationCode" and grantType "Password", and is# not allowed otherwise.clientID: "string"# A client secret must be specified for grantType "AuthorizationCode" and grantType "Password".# This can be done by including the raw secret as a string in "secret",# or by referencing Kubernetes secret with "secretName" (and "secretNamespace").# It is invalid to specify both "secret" and "secretName".secret: "string" # required (unless secretName is set)secretName: "string" # required (unless secret is set)secretNamespace: "string" # optional; default is the same namespace as the Filter# grantType "AuthorizationCode" uses cookies for session tracking. If useSessionCookies is true,# it will use session cookies, which are deleted when the browser is closed. If useSessionCookies# is false, the cookies can persist after the browser is closed.useSessionCookies: # optional; default is { value: false }value: bool # optional: default is trueifRequestHeader: # optional; default to apply "useSessionCookies.value" to all requestsname: "string" # requirednegate: bool # optional; default is false# It is invalid to specify both "value" and "valueRegex".value: "string" # optional; default is any non-empty stringvalueRegex: "regex-string" # optional; default is any non-empty string# HTTP client settings for talking with the identity providerinsecureTLS: bool # optional; default is falserenegotiateTLS: "enum-string" # optional; default is "never"maxStale: "duration-string" # optional; default is "0"
General settings:
grantType
: Which type of OAuth 2.0 authorization grant to request from the identity provider. Currently supported are:"AuthorizationCode"
: Authenticate by redirecting to a login page served by the identity provider."ClientCredentials"
: Authenticate by requiringX-Ambassador-Client-ID
andX-Ambassador-Client-Secret
HTTP headers on incoming requests, and using them to authenticate to the identity provider. Support for theClientCredentials
is currently preliminary, and only goes through limited testing."Password"
: Authenticate by requiringX-Ambassador-Username
andX-Ambassador-Password
on all incoming requests, and use them to authenticate with the identity provider using the OAuth2Resource Owner Password Credentials
grant type.
authorizationURL
: Identifies where to look for the/.well-known/openid-configuration
descriptor to figure out how to talk to the OAuth2 providerextraAuthorizationParameters
: Extra (non-standard or extension) OAuth authorization parameters to use. It is not valid to specify a parameter used by OAuth itself ("response_type", "client_id", "redirect_uri", "scope", or "state").accessTokenValidation
: How to verify the liveness and scope of Access Tokens issued by the identity provider. Valid values are either"auto"
,"jwt"
, or"userinfo"
. Empty or unset is equivalent to"auto"
."jwt"
: Validates the Access Token as a JWT.- By default: It accepts the RS256, RS384, or RS512 signature algorithms, and validates the signature against the JWKS from OIDC Discovery. It then validates the
exp
,iat
,nbf
,iss
(with the Issuer from OIDC Discovery), andscope
claims: if present, none of the scopes are required to be present. This relies on the identity provider using non-encrypted signed JWTs as Access Tokens, and configuring the signing appropriately - This behavior can be modified by delegating to
JWT
Filter withaccessTokenJWTFilter
. The arguments are the same as the arguments when referring to a JWT Filter from a FilterPolicy.
- By default: It accepts the RS256, RS384, or RS512 signature algorithms, and validates the signature against the JWKS from OIDC Discovery. It then validates the
"userinfo"
: Validates the access token by polling the OIDC UserInfo Endpoint. This means that the Ambassador Edge Stack must initiate an HTTP request to the identity provider for each authorized request to a protected resource. This performs poorly, but functions properly with a wider range of identity providers. It is not valid to setaccessTokenJWTFilter
ifaccessTokenValidation: userinfo
."auto"
attempts to do"jwt"
validation ifaccessTokenJWTFilter
is set or if the Access Token parses as a JWT and the signature is valid, and otherwise falls back to"userinfo"
validation.
Settings that are only valid for grantType: "AuthorizationCode"
or grantType: "Password"
:
clientID
: The Client ID you get from your identity provider.- The client secret you get from your identity provider can be specified 2 different ways:
- As a string, in the
secret
field. - As a Kubernetes
generic
Secret, named bysecretName
/secretNamespace
. The Kubernetes secret must of thegeneric
type, with the value stored under the keyoauth2-client-secret
. IfsecretNamespace
is not given, it defaults to the namespace of the Filter resource. - Note: It is invalid to set both
secret
andsecretName
.
- As a string, in the
Settings that are only valid when grantType: "AuthorizationCode"
:
clientURL
: (You determine this, and give it to your identity provider) Identifies a hostname that can appropriately set cookies for the application. Only the scheme (https://
) and authority (example.com:1234
) parts are used; the path part of the URL is ignored. You will also likely need to register${clientURL}/callback
as an authorized callback endpoint with your identity provider.
- By default, any cookies set by the Ambassador Edge Stack will be set to expire when the session expires naturally. Use the
useSessionCookies
setting to specify expiration on session cookies instead; the cookies will be deleted when the user closes their web browser.* However, this can prematurely delete cookies if the user closes their web browser. Conversely, it also means that cookies can persist for longer than normal if the user does not close their browser.* Any prematurely deleted cookies may or may not affect user-perceived behavior, depending onthe behavior of the identity provider.* Any cookies persisting longer will not affect behavior of the system; the Ambassador EdgeStack validates whether the session is expired when considering thecookie.- If
useSessionCookies
is non-null
, then by default it will have the cookies for all requests be session cookies or not according to theuseSessionCookies.value
sub-argument. Setting theifRequestHeader
sub-argument to usevalue
for requests that have (and!value
for requests that don't have) the HTTP header fieldname
(case-insensitive) either set to (ifnegate: false
) or not set to (ifnegate: true
)- a non-empty string if neither
value
norvalueRegex
are set - the exact string
value
(case-sensitive) (ifvalue
is set) - a string that matches the regular expression
valueRegex
(ifvalueRegex
is set). This uses RE2 syntax (always, not obeyingregex_type
in the Ambassador module) but does not support the\C
escape sequence.
- a non-empty string if neither
- If
HTTP client settings for talking to the identity provider:
maxStale
: How long to keep stale cached OIDC replies for. This sets themax-stale
Cache-Control directive on requests, and also ignores theno-store
andno-cache
Cache-Control directives on responses. This is useful for maintaining good performance when working with identity providers with misconfigured Cache-Control.insecureTLS
disables TLS verification when speaking to an identity provider with anhttps://
authorizationURL
. This is discouraged in favor of either using plainhttp://
or installing a self-signed certificate.renegotiateTLS
allows a remote server to request TLS renegotiation. Accepted values are "never", "onceAsClient", and "freelyAsClient".
"duration-string"
strings are parsed as a sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". See Go time.ParseDuration
.
OAuth2
Path-Specific Arguments
---apiVersion: getambassador.io/v2kind: FilterPolicymetadata:name: "example-filter-policy"namespace: "example-namespace"spec:rules:- host: "*"path: "*"filters:- name: "example-oauth2-filter"arguments:scopes: # optional; default is ["openid"] for `grantType=="AuthorizationCode"`; [] for `grantType=="ClientCredentials"` and `grantType=="Password"`- "scope1"- "scope2"insteadOfRedirect: # optional for "AuthorizationCode"; default is to do a redirect to the identity providerifRequestHeader: # optional; default is to return httpStatusCode for all requests that would redirect-to-identity-providername: "string" # requirednegate: bool # optional; default is false# It is invalid to specify both "value" and "valueRegex".value: "string" # optional; default is any non-empty stringvalueRegex: "regex-string" # optional; default is any non-empty string# option 1:httpStatusCode: integer # optional; default is 403 (unless `filters` is set)# option 2:filters: # optional; default is to use `httpStatusCode` instead- name: "string" # requirednamespace: "string" # optional; default is the same namespace as the FilterPolicyifRequestHeader: # optional; default to apply this filter to all requests matching the host & pathname: "string" # requirednegate: bool # optional; default is false# It is invalid to specify both "value" and "valueRegex".value: "string" # optional; default is any non-empty stringvalueRegex: "regex-string" # optional; default is any non-empty stringonDeny: "enum-string" # optional; default is "break"onAllow: "enum-string" # optional; default is "continue"arguments: DEPENDS # optional
scopes
: A list of OAuth scope values to include in the scope of the authorization request. If one of the scope values for a path is not granted, then access to that resource is forbidden; if thescopes
argument listsfoo
, but the authorization response from the provider does not includefoo
in the scope, then it will be taken to mean that the authorization server forbade access to this path, as the authenticated user does not have thefoo
resource scope.If
grantType: "AuthorizationCode"
, then theopenid
scope value is always included in the requested scope, even if it is not listed in theFilterPolicy
argument.If
grantType: "ClientCredentials"
orgrantType: "Password"
, then the default scope is empty. If your identity provider does not have a default scope, then you will need to configure one here.As a special case, if the
offline_access
scope value is requested, but not included in the response then access is not forbidden. With many identity providers, requesting theoffline_access
scope is necessary to receive a Refresh Token.The ordering of scope values does not matter, and is ignored.
insteadOfRedirect
: An action to perform instead of redirecting the User-Agent to the identity provider, when usinggrantType: "AuthorizationCode"
. By default, if the User-Agent does not have a currently-authenticated session, then the Ambassador Edge Stack will redirect the User-Agent to the identity provider. SettinginsteadOfRedirect
allows you to modify this behavior.insteadOfRedirect
does nothing whengrantType: "ClientCredentials"
, because the Ambassador Edge Stack will never redirect the User-Agent to the identity provider for the client credentials grant type.- If
insteadOfRedirect
is non-null
, then by default it will apply to all requests that would cause the redirect; setting theifRequestHeader
sub-argument causes it to only apply to requests that have the HTTP header fieldname
(case-insensitive) either set to (ifnegate: false
) or not set to (ifnegate: true
)- a non-emtpy string if neither
value
norvalueRegex
are set - the exact string
value
(case-sensitive) (ifvalue
is set) - a string that matches the regular expression
valueRegex
(ifvalueRegex
is set). This uses RE2 syntax (always, not obeyingregex_type
in the Ambassador module) but does not support the\C
escape sequence.
- a non-emtpy string if neither
- By default, it serves an authorization-denied error page; by default HTTP 403 ("Forbidden"), but this can be configured by the
httpStatusCode
sub-argument. - Instead of serving that simple error page, it can instead be configured to call out to a list of other Filters, by setting the
filters
list. The syntax and semantics of this list are the same as.spec.rules[].filters
in aFilterPolicy
. Be aware that if one of these filters modify the request rather than returning a response, then the request will be allowed through to the backend service, even though theOAuth2
Filter denied it. - It is invalid to specify both
httpStatusCode
andfilters
.
- If
XSRF protection
The ambassador_xsrf.NAME.NAMESPACE
cookie is an opaque string that should be used as an XSRF token. Applications wishing to leverage the Ambassador Edge Stack in their XSRF attack protection should take two extra steps:
- When generating an HTML form, the server should read the cookie, and include a
<input type="hidden" name="_xsrf" value="COOKIE_VALUE" />
element in the form. - When handling submitted form data should verify that the form value and the cookie value match. If they do not match, it should refuse to handle the request, and return an HTTP 4XX response.
Applications using request submission formats other than HTML forms should perform analogous steps of ensuring that the value is present in the request duplicated in the cookie and also in either the request body or secure header field. A secure header field is one that is not Cookie
, is not "simple", and is not explicitly allowed by the CORS policy.
Note: Prior versions of the Ambassador Edge Stack did not have an
ambassador_xsrf.NAME.NAMESPACE
cookie, and instead required you to
use the ambassador_session.NAME.NAMESPACE
cookie. The
ambassador_session.NAME.NAMESPACE
cookie should no longer be used
for XSRF-protection purposes
RP-initiated logout
When a logout occurs, it is often not enough to delete the Ambassador Edge Stack's session cookie or session data; after this happens, and the web browser is redirected to the Identity Provider to re-log-in, the Identity Provider may remember the previous login, and immediately re-authorize the user; it would be like the logout never even happened.
To solve this, the Ambassador Edge Stack can use OpenID Connect Session Management to perform an "RP-Initiated Logout", where the Ambassador Edge Stack (the OpenID Connect "Relying Party" or "RP") communicates directly with Identity Providers that support OpenID Connect Session Management, to properly log out the user. Unfortunately, many Identity Providers do not support OpenID Connect Session Management.
This is done by having your application direct the web browser POST
and navigate to /.ambassador/oauth2/logout
. There are 2
form-encoded values that you need to include:
realm
: Thename.namespace
of theFilter
that you want to log out of. This may be submitted as part of the POST body, or may be set as a URL query parameter._xsrf
: The value of theambassador_xsrf.{{realm}}
cookie (where{{realm}}
is as described above). This must be set in the POST body, the URL query part will not be checked.
For example:
<form method="POST" action="/.ambassador/oauth2/logout" target="_blank"><input type="hidden" name="realm" value="myfilter.mynamespace" /><input type="hidden" name="_xsrf" value="{{ .Cookie.Value }}" /><input type="submit" value="Log out" /></form>
or
<form method="POST" action="/.ambassador/oauth2/logout?realm=myfilter.mynamespace" target="_blank"><input type="hidden" name="_xsrf" value="{{ .Cookie.Value }}" /><input type="submit" value="Log out" /></form>
or from JavaScript
function getCookie(name) {var prefix = name + "=";var cookies = document.cookie.split(';');for (var i = 0; i < cookies.length; i++) {var cookie = cookies[i].trimStart();if (cookie.indexOf(prefix) == 0) {return cookie.slice(prefix.length);}}return "";}function logout(realm) {var form = document.createElement('form');form.method = 'post';form.action = '/.ambassador/oauth2/logout?realm='+realm;//form.target = '_blank'; // uncomment to open the identity provider's page in a new tabvar xsrfInput = document.createElement('input');xsrfInput.type = 'hidden';xsrfInput.name = '_xsrf';xsrfInput.value = getCookie("ambassador_xsrf."+realm);form.appendChild(xsrfInput);document.body.appendChild(form);form.submit();}
Redis
The Ambassador Edge Stack relies on Redis to store short-lived authentication credentials and rate limiting information. If the Redis data store is lost, users will need to log back in and all existing rate-limits would be reset.
Further reading
In this architecture, Ambassador Edge Stack is functioning as an Identity Aware Proxy in a Zero Trust Network. For more about this security architecture, read the BeyondCorp security architecture whitepaper by Google.
The "How-to" section has detailed tutorials on integrating Ambassador with a number of Identity Providers.
Questions?
We’re here to help. If you have questions, join our Slack or contact us.