Project: Skipper (Orchestration & Management / Service Proxy)
File: auth.md

Basic Auth is defined in . 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`: ```sh apt-get install apache2-utils ``` Create a htpasswd file `foo.passwd` and use `captain` with password `apassword`: ```sh htpasswd -bcB foo.passwd captain apassword ``` Start skipper with a `basicAuth` filter referencing the just created htpasswd file: ```sh ./bin/skipper -address :8080 -inline-routes 'r: * -> basicAuth("foo.passwd") -> status(200) -> <shunt>' ``` A client request without login credentials or wrong credentials: ```sh % 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: ```sh % 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 ``` 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 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: ```sh 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 service to service authentication and authorization is specified by . Skipper uses [RFC Draft for discovering token infrastructure configuration](https://tools.ietf.org/html/draft-ietf-oauth-discovery-06), to find the `introspection_endpoint`. Example route: ```sh 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 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 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

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. ```sh 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. ```sh oauthOidcUserInfo("https://oidc-provider.example.com", "clientid", "clientsecret", "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

```json { "oauth2token": "xxx", "claims": { "claim1": "val1", "claim2": "val2" }, "subject": "subj" } ``` In the case of a user info filter the payload is in the format: ```json { "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. Authorization validation and access control is available by means of a subsequent filter . 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: ```json { "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\"") ``` 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`<sup></sup>. If it does not, or if the cookie and its tokens are invalid, it redirects the user to the OAuth2 provider's authorization endpoint<sup></sup>. 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 redirect<sup></sup>. The callback route must have a `grantCallback()` 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 . 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`<sup></sup> 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. <sup><a name="grant-note-1">1</a></sup> The name of this cookie can be changed by providing the `-oauth2-token-cookie-name` parameter. <sup><a name="grant-note-2">2</a></sup> The value of `redirecturi` parameter of the authorization flow could be set by providing `-oauth2-auth-url-parameters=redirecturi=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. 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. The `oauthGrant()` filter also supports token

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. To use authorization grant flow, you need to: 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: ```sh skipper -oauth2-client-id-file=/path/to/client_id \ -oauth2-client-secret-file=/path/to/client_secret \ -oauth2-secret-file=/path/to/cookieencryptionsecret \ -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: ```sh skipper -oauth2-client-id=<CLIENTID> -oauth2-client-secret=<CLIENTSECRET> ``` 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: ```sh skipper -enable-oauth2-grant-flow \ -oauth2-auth-url=<OAUTH2AUTHORIZEENDPOINT> \ -oauth2-token-url=<OAUTH2TOKENENDPOINT> \ -oauth2-revoke-token-url=<OAUTH2REVOKETOKEN_ENDPOINT> \ -oauth2-tokeninfo-url=<OAUTH2TOKENINFOENDPOINT> \ -oauth2-callback-path=/oauth/callback ``` The `-oauth2-revoke-token-url` is optional, and should be supplied if you plan to use the filter to revoke tokens. You can configure the `oauthGrant()` filter further for your needs. See the filter reference for more details. 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: ```sh foo: Path("/foo") -> oauthGrant() -> "http://localhost:9090"; logout: Path("/logout) -> grantLogout() -> redirectTo(302) -> <shunt>; ``` You can add a filter after a to control access based on any OAuth2 claim. A claim is any property returned by the tokeninfo endpoint. The filter works exactly like the filter (it is actually just an alias for

For example, if your tokeninfo endpoint returns the following JSON: ```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 the `oauthGrant` filter with the following query: ```sh -> 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: ```sh 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 is `username`. To enable 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 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. As an example the following initial config can be used ```yaml 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 to policies follow those that are used by the , the existing apply also to Skipper. Please note that the filters in Skipper always generate v3 input structures. 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"` A quick way without setting up Backend APIs is to use the . 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` ```yaml bundles: play: resource: bundles/{{ .bundlename }} polling: longpollingtimeout_seconds: 45 services: name: play url: https://play.openpolicyagent.org plugins: envoyextauthz_grpc: 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`