Authentication and Authorization
Basic auth¶
Basic Auth is defined in RFC7617.
Install htpasswd command line tool, we assume Debian based
system. Please refer the documentation of your Operating System or
package management vendor how to install htpasswd
:
apt-get install apache2-utils
Create a htpasswd file foo.passwd
and use captain
with password apassword
:
htpasswd -bcB foo.passwd captain apassword
Start skipper with a basicAuth
filter referencing the just created
htpasswd file:
./bin/skipper -address :8080 -inline-routes 'r: * -> basicAuth("foo.passwd") -> status(200) -> <shunt>'
A client request without login credentials or wrong credentials:
% curl localhost:8080/ -v
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Server: Skipper
< Www-Authenticate: Basic realm="Basic Realm"
< Date: Thu, 01 Nov 2018 21:27:18 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
A client request with the correct credentials:
% curl captain:apassword@localhost:8080/ -v
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'captain'
> GET / HTTP/1.1
> Host: localhost:8080
> Authorization: Basic Y2FwdGFpbjphcGFzc3dvcmQ=
> User-Agent: curl/7.49.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Skipper
< Date: Thu, 01 Nov 2018 21:29:21 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Token service-to-service¶
Service to service authentication and authorization is often done by using the HTTP Authorization header with the content prefix “Bearer “, for example “Authorization: Bearer mytoken”.
Supported token formats
Tokeninfo¶
Tokeninfo is a common, but not specified protocol, only supporting Bearer tokens in the Authorization header.
In most cases you would have to have your own OAuth2 token infrastructure, that can return JWT or OAuth2 access tokens to authenticated parties and validate tokens with their custom tokeninfo endpoint. In case of JWT the access token is signed and can be validated without a central tokeninfo endpoint.
Example route:
all: Path("/")
-> oauthTokeninfoAnyScope("read-X", "readwrite-X")
-> "http://localhost:9090/"
The access token should be passed from the client as Bearer token in the Authorization header. Skipper will send this token unchanged as Bearer token in the Authorization header to the Tokeninfo endpoint. The request flow with a Tokeninfo setup is shown in the following picture:
Tokenintrospection RFC7662¶
Tokenintrospection service to service authentication and authorization
is specified by RFC7662.
Skipper uses RFC Draft for discovering token infrastructure
configuration,
to find the introspection_endpoint
.
Example route:
all: *
-> oauthTokenintrospectionAnyKV("https://identity.example.com/managed-id", "jdoe")
-> "http://localhost:9090/";
The access token should be passed from the client as Bearer token in
the Authorization header. Skipper will send this token as
defined in RFC7662
in a POST request “application/x-www-form-urlencoded” as value for key
token
to the Tokenintrospection endpoint.
The request flow with Tokenintrospection setup is shown in the
following picture:
OpenID Connect¶
OpenID Connect is an OAuth2.0 based authentication and authorization mechanism supported by several providers. Skipper can act as a proxy for backend server which requires authenticated clients. Skipper handles the authentication with the provider and upon successful completion of authentication passes subsequent requests to the backend server.
Skipper’s implementation of OpenID Connect Client works as follows:
- Filter is initialized with the following parameters:
- Secrets file with keys used for encrypting the token in a cookie and also for generating shared secret.
- OpenID Connect Provider URL
- The Client ID
- The Client Secret
- The Callback URL for the client when a user successfully authenticates and is returned.
- The Scopes to be requested along with the
openid
scope - The claims that should be present in the token or the fields need in the user information.
- The user makes a request to a backend which is covered by an OpenID filter.
- Skipper checks if a cookie is set with any previous successfully completed OpenID authentication.
- If the cookie is valid then Skipper passes the request to the backend.
- If the cookie is not valid then Skipper redirects the user to the OpenID provider with its Client ID and a callback URL.
- When the user successfully completes authentication the provider redirects the user to the callback URL with a token.
- Skipper receives this token and makes a backend channel call to get an ID token and other required information.
- If all the user information/claims are present then it encrypts this and sets a cookie which is encrypted and redirects the user to the originally requested URL.
To use OpenID define a filter for a backend which needs to be covered by OpenID Connection authentication.
oauthOidcAllClaims("https://accounts.identity-provider.com", "some-client-id",
"some-client-secret", "http://callback.com/auth/provider/callback", "scope1 scope2",
"claim1 claim2") -> "https://internal.example.org";
Here scope1 scope2
are the scopes that should be included which requesting authentication from the OpenID provider.
Any number of scopes can be specified here. The openid
scope is added automatically by the filter. The other fields
which need to be specified are the URL of the provider which in the above example is
https://accounts.identity-provider.com
. The client ID and the client secret. The callback URL which is specified
while generating the client id and client secret. Then the scopes and finally the claims which should be present along
with the return id token.
oauthOidcUserInfo("https://oidc-provider.example.com", "client_id", "client_secret",
"http://target.example.com/subpath/callback", "email profile",
"name email picture") -> "https://internal.example.org";
This filter is similar but it verifies that the token has certain user information
information fields accessible with the token return by the provider. The fields can
be specified at the end like in the example above where the fields name
, email
and picture
are requested.
Upon successful authentication Skipper will start allowing the user requests through
to the backend. Along with the original request to the backend Skipper will include
information which it obtained from the provider. The information is in JSON
format
with the header name Skipper-Oidc-Info
. In the case of the claims container the
header value is in the format.
{
"oauth2token": "xxx",
"claims": {
"claim1": "val1",
"claim2": "val2"
},
"subject": "subj"
}
In the case of a user info filter the payload is in the format:
{
"oauth2token": "xxx",
"userInfo": {
"sub": "sub",
"profile": "prof",
"email": "abc@example.com",
"email_verified": "abc@example.com"
},
"subject": "subj"
}
Skipper encrypts the cookies and also generates a nonce during the OAuth2.0 flow
for which it needs a secret key. This key is in a file which can be rotated periodically
because it is reread by Skipper. The path to this file must be passed with the flag
-oidc-secrets-file
when Skipper is started.
AuthZ and access control¶
Authorization validation and access control is available by means of a subsequent filter oidcClaimsQuery. It inspects the ID token, which exists after a successful oauthOidc*
filter step, and validates the defined query with the request path.
Given following example ID token:
{
"email": "someone@example.org",
"groups": [
"CD-xyz",
"appX-Tester"
],
"name": "Some One"
}
Access to path /
would be granted to everyone in example.org
, however path /login
only to those being member of group "appX-Tester"
:
oauthOidcAnyClaims(...) -> oidcClaimsQuery("/login:groups.#[==\"appX-Tester\"]", "/:@_:email%\"*@example.org\"")
OAuth2 authorization grant flow¶
Authorization grant flow is a mechanism
to coordinate between a user-agent, a client, and an authorization server to obtain an OAuth2
access token for a user. Skipper supports the flow with the oauthGrant()
filter.
It works as follows:
- A user makes a request to a route with
oauthGrant()
. - The filter checks whether the request has a cookie called
oauth-grant
1. If it does not, or if the cookie and its tokens are invalid, it redirects the user to the OAuth2 provider’s authorization endpoint2. - The user logs into the external OAuth2 provider, e.g. by providing a username and password.
- The provider redirects the user back to Skipper with an authorization code, using the
redirect_uri
URL parameter which was part of the previous redirect2. The callback route must have agrantCallback()
filter defined. Skipper automatically adds this callback route for you when the OAuth2 authorization grant flow feature is enabled. Note that the automatically added callback route does not apply default filters. If you need default filters to be applied to the callback route as well, please register the route manually in your routes files. - Skipper calls the provider’s token URL with the authorization code, and receives a response with the access and refresh tokens.
- Skipper stores the tokens in an
oauth-grant
1 cookie which is stored in the user’s browser. - Subsequent calls to any route with an
oauthGrant()
filter will now pass as long as the access token is valid.
1 The name of this cookie can be changed by providing the -oauth2-token-cookie-name
parameter.
2 The value of redirect_uri
parameter of the authorization flow could be set by providing -oauth2-auth-url-parameters=redirect_uri=https://example.org/oauth-callback
.
If not set Skipper will automatically determine it based on the initial request hostname and -oauth2-callback-path
flag value.
Please note that it is not currently possible to use multiple OAuth2 providers with Skipper.
Encrypted cookie tokens¶
The cookie set by the oauthGrant()
filter contains the OAuth2 access and refresh tokens in
encrypted form. This means Skipper does not need to persist any session information about users,
while also not exposing the tokens to users.
Token refresh¶
The oauthGrant()
filter also supports token refreshing. Once the access token expires and
the user makes another request, the filter automatically refreshes the token and sets the
updated cookie in the response.
Instructions¶
To use authorization grant flow, you need to:
- Configure OAuth2 credentials.
- Configure the grant filters with OAuth2 URLs.
- Add the OAuth2 grant filters to routes.
Configure OAuth2 credentials¶
Before you start, you need to register your application with the OAuth2 provider.
If your provider asks you for the callback URL, provide the URL that you set
as the -oauth2-callback-path
parameter. If you did not provide a value, use the default
route : /.well-known/oauth2-callback
.
Skipper must be configured with the following credentials and secrets:
- OAuth2 client ID for authenticating with the OAuth2 provider.
- OAuth2 client secret for authenticating with the OAuth2 provider.
- Cookie encryption secret for encrypting and decrypting token cookies.
You can load all of these secrets from separate files, in which case they get automatically reloaded to support secret rotation. You can provide the paths to the files containing each secret as follows:
skipper -oauth2-client-id-file=/path/to/client_id \
-oauth2-client-secret-file=/path/to/client_secret \
-oauth2-secret-file=/path/to/cookie_encryption_secret \
-credentials-update-interval=30s
Paths may contain {host}
placeholder which will be replaced by the request host.
This is used to define separate credentials for different hosts.
Care must be taken when used in conjunction with -credentials-paths
option because files
from -credentials-paths
are available to bearerinjector
filter.
That is -credentials-paths=/path/to
in above example will expose grant files to bearerinjector
filter.
You can modify the secret update interval using the -credentials-update-interval
argument. In
example above, the interval is configured to reload the secrets from the files every 30
seconds.
If you prefer, you can provide the client ID and secret values directly as arguments to Skipper instead of loading them from files. In that case, call Skipper with:
skipper -oauth2-client-id=<CLIENT_ID> -oauth2-client-secret=<CLIENT_SECRET>
Configure the grant filters¶
The grant filters need to be enabled and configured with your OAuth2 provider’s authorization, token, and tokeninfo endpoints. This can be achieved by providing Skipper with the following arguments:
skipper -enable-oauth2-grant-flow \
-oauth2-auth-url=<OAUTH2_AUTHORIZE_ENDPOINT> \
-oauth2-token-url=<OAUTH2_TOKEN_ENDPOINT> \
-oauth2-revoke-token-url=<OAUTH2_REVOKE_TOKEN_ENDPOINT> \
-oauth2-tokeninfo-url=<OAUTH2_TOKENINFO_ENDPOINT> \
-oauth2-callback-path=/oauth/callback
The -oauth2-revoke-token-url
is optional, and should be supplied if you plan
to use the grantLogout filter to revoke tokens.
You can configure the oauthGrant()
filter further for your needs. See the
oauthGrant filter reference for more details.
Add filters to your routes¶
You can protect any number of routes with the oauthGrant()
filter. Unauthenticated users
will be refused access and redirected to log in.
Skipper will automatically add a callback route for you with the grantCallback
filter registered
on it. The path for this route can be configured with the -oauth2-callback-path
parameter.
If the parameter is not given, it will be /.well-known/oauth2-callback
You can optionally add a grantLogout()
filter to delete token cookie.
If -oauth2-revoke-token-url
is set it will revoke access and refresh tokens:
foo:
Path("/foo")
-> oauthGrant()
-> "http://localhost:9090";
logout:
Path("/logout)
-> grantLogout()
-> redirectTo(302)
-> <shunt>;
(Optional) AuthZ and access control¶
You can add a grantClaimsQuery filter after a oauthGrant to control access based on any OAuth2 claim. A claim is any property returned by the tokeninfo endpoint. The filter works exactly like the oidcClaimsQuery filter (it is actually just an alias for it).
For example, if your tokeninfo endpoint returns the following JSON:
{
"scope": ["email"],
"username": "foo"
}
you could limit the access to a given route only to users that have the email
scope by doing the following:
- Append a
grantClaimsQuery
filter to theoauthGrant
filter with the following query:-> oauthGrant() -> grantClaimsQuery("/path:scope.#[==\"email\"]")
- Provide the name of the claim that corresponds to the OAuth2 subject in the
tokeninfo payload as an argument to Skipper:
skipper -oauth2-tokeninfo-subject-key=username
The subject is the field that identifies the user and is often called
sub
, especially in the context of OpenID Connect. In the example above, it isusername
.
Open Policy Agent¶
To enable Open Policy Agent filter, use the -enable-open-policy-agent
command line flag.
Open Policy Agent is integrated as a Go library so no extra setup is needed to run. Every filter creates a virtual OPA instance in memory that is configured using a configuration file in the same configuration format that a standalone OPA would use. To allow for configurability, the configuration file is interpolated using Go Templates to allow every virtual instance to pull different bundles. This template file is passed using the -open-policy-agent-config-template
flag.
Configuration File¶
As an example the following initial config can be used
services:
- name: bundle-service
url: https://my-example-opa-bucket.s3.eu-central-1.amazonaws.com
credentials:
s3_signing:
environment_credentials: {}
labels:
environment: production
discovery:
name: discovery
prefix: "/applications/{{ .bundlename }}"
The variable .bundlename
is the first argument in the following filters and can be in any format that OPA can understand, so for example application IDs from a registry, uuids, …
Input Structures¶
Input structures to policies follow those that are used by the opa-envoy-plugin, the existing examples and documentation apply also to Skipper. Please note that the filters in Skipper always generate v3 input structures.
Passing context to the policy¶
Generally there are two ways to pass context to a policy:
- as part of the labels in Open Policy Agent (configured in the configuration file, see below) that should be used for deployment level taxonomy,
- as part of so called context extensions that are part of the Envoy external auth specification.
This context can be passed as second argument to filters:
opaAuthorizeRequest("my-app-id", "com.mycompany.myprop: myvalue")
or opaAuthorizeRequest("my-app-id", "{'com.mycompany.myprop': 'my value'}")
The second argument is parsed as YAML, cannot be nested and values need to be strings.
In Rego this can be used like this input.attributes.contextExtensions["com.mycompany.myprop"] == "my value"
Decision ID in Policies¶
Each evaluation yields a distinct decision, identifiable by its unique decision ID.
This decision ID can be located within the input at:
input.attributes.metadataContext.filterMetadata.open_policy_agent.decision_id
Typical use cases are either propagation of the decision ID to downstream systems or returning it as part of the response. As an example this can allow to trouble shoot deny requests by looking up details using the full decision in a control plane.
Quick Start Rego Playground¶
A quick way without setting up Backend APIs is to use the Rego Playground.
To get started pick from examples Envoy > Hello World. Click on “Publish” and note the random ID in the section “Run OPA with playground policy”.
Place the following file in your local directory with the name opaconfig.yaml
bundles:
play:
resource: bundles/{{ .bundlename }}
polling:
long_polling_timeout_seconds: 45
services:
- name: play
url: https://play.openpolicyagent.org
plugins:
envoy_ext_authz_grpc:
# This needs to match the package, defaulting to envoy/authz/allow
path: envoy/http/public/allow
dry-run: false
decision_logs:
console: true
Start Skipper with
skipper -enable-open-policy-agent -open-policy-agent-config-template opaconfig.yaml \
-inline-routes 'notfound: * -> opaAuthorizeRequest("<playground-bundle-id>") -> inlineContent("<h1>Authorized Hello</h1>") -> <shunt>'
You can test the policy with
- Authorized:
curl http://localhost:9090/ -i
- Authorized:
curl http://localhost:9090/foobar -H "Authorization: Basic charlie" -i
- Forbidden:
curl http://localhost:9090/foobar -i