Skip to content

OAuth login for functions

The of-watchdog can protect a function with browser login through an OAuth 2.0 or OpenID Connect (OIDC) provider. It handles sign-in and checks the browser's session before forwarding requests to the function.

Example: Deploy a function with OIDC login

This walkthrough deploys a function that requires visitors to sign in through an OIDC provider. It shows how to configure the watchdog to enable OAuth without changing the function handler.

  1. Create a function with the golang-middleware template:

    faas-cli template store pull golang-middleware
    faas-cli new profile --lang golang-middleware
    

    The watchdog handles login, so the function does not need to implement any login or OAuth logic in its handler.

  2. Register a client with your OIDC provider.

    Set its redirect (callback) URI to https://gateway.example.com/function/profile/auth/callback. Note its client ID and client secret. The client secret is optional; public clients using PKCE are also supported.

  3. Create a signing key as an OpenFaaS secret:

    faas-cli secret generate | faas-cli secret create profile-signing-key \
      --from-file=/dev/stdin --gateway=https://gateway.example.com
    

    The watchdog uses this key to sign its own JWT session cookie, which it sets as an HttpOnly cookie after successful authentication.

    Save the client secret from your provider in a local file named client-secret, then create an OpenFaaS secret from it:

    faas-cli secret create profile-client-secret --from-file=client-secret \
      --gateway=https://gateway.example.com
    
  4. Configure stack.yaml to enable login and mount both secrets:

    version: 1.0
    provider:
      name: openfaas
      gateway: https://gateway.example.com
    functions:
      profile:
        lang: golang-middleware
        handler: ./profile
        image: ttl.sh/openfaas-examples/profile:latest
        environment:
          oauth_enabled: 'true'
          oauth_base_url: https://gateway.example.com/function/profile
          oauth_client_id: profile
          oauth_issuer_url: https://keycloak.example.com/realms/openfaas
          oauth_signing_key: profile-signing-key
          oauth_client_secret: profile-client-secret
        secrets:
          - profile-signing-key
          - profile-client-secret
    

    Replace the gateway URL, image, client ID, and issuer URL with your own values.

    • oauth_base_url is the public URL where visitors reach the function, normally <gateway-url>/function/<function-name> (here, https://gateway.example.com/function/profile). The watchdog needs it to redirect visitors back after login and to build the correct OAuth callback URL.
    • oauth_client_id is the client ID assigned by the provider when the application is registered.
    • oauth_issuer_url is the provider's issuer URL, which the watchdog uses to discover its authorization, token, and public-key endpoints.
    • oauth_signing_key is the name of the secret with the signing key.
    • oauth_client_secret is the name of the secret holding the provider-issued client secret.

    For a public client using PKCE without a client secret, omit oauth_client_secret and its entry in secrets.

  5. Build and deploy the function:

    faas-cli up --tag=sha
    

    Visit https://gateway.example.com/function/profile. Without a valid cookie, the watchdog shows its sign-in page. When Sign in is clicked, the watchdog redirects to the configured OAuth provider. After login, the browser sends the session cookie with each request to the function. The watchdog validates it before forwarding the request to the handler, enforcing authentication.

    To sign out, have the visitor's browser send a POST to {oauth_base_url}/auth/logout, for example with a sign-out form. The watchdog clears the browser's session cookie and returns the visitor to the sign-in page.

  6. Optional: Add authorization rules to the handler.

    The watchdog checks for a valid session cookie from a successful login with the provider. It does not decide whether that visitor's identity may access the function or specific resources.

    The handler must enforce those rules by verifying the session cookie and checking the identity claims it contains, such as sub.

Reference

Required configuration

Set oauth_enabled=true and configure the function's public URL, OIDC client ID, issuer URL, and signing key. For a provider without OIDC discovery (e.g. GitHub OAuth), use both endpoint options instead of oauth_issuer_url.

Option Usage
oauth_enabled Set to true to enable OAuth/OIDC login and session validation. Default: false.
oauth_base_url The function's public URL, including its path, such as https://gateway.example.com/function/profile. Used to build the callback URL, cookie path, and session issuer/audience. Register {oauth_base_url}/auth/callback with the provider.
oauth_client_id The client ID registered with the provider.
oauth_issuer_url The OIDC provider's issuer URL. The watchdog discovers its authorization, token, and keys endpoints and validates ID tokens.
oauth_authorization_endpoint The provider's authorization URL. Use together with oauth_token_endpoint for providers without OIDC discovery, such as a GitHub OAuth App.
oauth_token_endpoint The provider's token exchange URL. Use together with oauth_authorization_endpoint for providers without OIDC discovery.
oauth_signing_key Name of an OpenFaaS secret containing a base64-encoded random 32-byte key used to sign session cookies.

Optional configuration

Option Usage
oauth_client_secret Name of an OpenFaaS secret containing the client secret. Omit for public clients without a secret that support PKCE.
oauth_scopes Space- or comma-separated scopes. Default: openid. OIDC always includes openid.
oauth_groups Optional comma-separated list of exact, case-sensitive provider group names to copy into fed:groups. Entries are trimmed and de-duplicated. Unset or blank imports all groups.
oauth_cookie_name Session cookie name. Default: of_session. Must be a valid cookie name and differ from the login cookie name.
oauth_login_cookie_name Temporary login cookie name. Default: of_login. Must be a valid cookie name and differ from the session cookie name.
oauth_login_redirect Destination after successful login. Defaults to oauth_base_url.
oauth_session_default_ttl Session lifetime when the provider supplies no expiry. Default: 1h.
oauth_session_ttl Optional override for the session JWT and cookie lifetime, even beyond provider token expiry. The provider is not contacted again during the session. When unset, the ID token expiry, OAuth expires_in, or the default lifetime is used.
oauth_allow_http Allow HTTP provider endpoints, discovery, and redirects for development. Default: false (HTTPS required).
oauth_token_auth_method Client-secret authentication method: client_secret_basic (default) or client_secret_post. Unused without a client secret.

Both session lifetime options accept positive Go durations in whole seconds, such as 30m or 8h.

Login routes

The watchdog serves these routes under the function's public URL:

Method and path Purpose
GET /auth/login Show the built-in login page
POST /auth/login Start login and redirect the browser to the provider
GET /auth/callback Complete login and set the session cookie
POST /auth/logout Clear the session cookie and return to the login page

The login page has a button that starts the provider flow. After successful login, the watchdog sets a signed, HttpOnly session cookie (default name of_session) and sends the browser back to the function. Requests with no valid session cookie return to the login page, while login failures show a built-in error page. Logout clears the function's cookies but does not end the session at the provider.

After a successful login, the watchdog forwards the session cookie with each authenticated request. The cookie contains a JWT issued by the watchdog and signed with HS256 using the key in oauth_signing_key.

The provider's access and ID tokens are discarded once login completes and are never stored in the cookie. In OIDC mode, the watchdog verifies the ID token before retaining a few identity claims, following the same convention as OpenFaaS IAM. The decoded payload looks like this:

{
  "iss": "https://gateway.example.com/function/profile",
  "sub": "fed:8a2f6c1e-4d7b-4f0a-9c3e-2b5d7e9f1a4c",
  "aud": ["https://gateway.example.com/function/profile"],
  "exp": 1800003600,
  "iat": 1800000000,
  "typ": "session",
  "fed:iss": "https://keycloak.example.com/realms/openfaas",
  "fed:email": "alice@example.com",
  "fed:email_verified": true,
  "fed:name": "Alice",
  "fed:groups": ["admins", "staff"]
}
Claim Meaning
iss, aud The function's public URL from oauth_base_url. These identify the watchdog-issued session, not the OIDC provider.
iat, exp Unix timestamps for when the session was issued and when it expires.
typ The watchdog token type. A session JWT always has the value session.
sub The provider's subject, prefixed with fed:. Use this as the stable user identifier.
fed:iss The provider's issuer URL.
fed:email, fed:email_verified, fed:name Copied from the ID token when present. Only use the email for authorization when fed:email_verified is true and the provider's email semantics are suitable.
fed:groups Provider groups, optionally selected with oauth_groups, in provider order.
fed:groups_truncated When true, the group information is incomplete because the provider claim contained unusable values or the selected groups did not all fit in the cookie. Absence from fed:groups is then not proof of non-membership.

With plain OAuth, for example a GitHub OAuth App, the session contains only the watchdog's registered claims and typ: session; it proves that the visitor signed in with the provider, but not who they are. Any unexpected id_token returned in this mode is ignored because it has not passed OIDC verification. If the function needs the visitor's identity, configure an OIDC provider with oauth_issuer_url.

By default, the watchdog copies all string values from the provider's groups claim. Non-string entries are omitted; if the claim is not an array or contains a non-string entry, the watchdog sets fed:groups_truncated: true. If the handler only needs a small set, configure it explicitly:

environment:
  oauth_groups: admins, staff

Selection happens before cookie sizing, so the required groups are retained even when they appeared near the end of a large provider claim. If the selected groups still make the JWT exceed the cookie budget, the watchdog removes groups from the end, sets the same truncation marker, and logs only the included and omitted counts. Presence in fed:groups can support an allow decision. When the marker is true, treat the group set as unknown and deny the request if the decision depends on a group that is absent.

The function never receives the provider's access token, so it cannot call the provider's APIs on the visitor's behalf. If it needs to, the function must implement its own OAuth flow instead of using the watchdog.

Use of-watchdog 0.12.5 or later

Version 0.12.4 introduced the flat session format, but explicit plain-OAuth endpoint mode could promote claims from an unverified id_token. Version 0.12.5 ignores identity data in plain OAuth and only copies claims returned by OIDC verification. The session schema is unchanged from 0.12.4. If explicit endpoints returned an ID token under 0.12.4, rotate oauth_signing_key during the upgrade to invalidate those sessions immediately. Version 0.12.3 used an intermediate nested format, and 0.12.2 and earlier stored provider tokens in the cookie; rotate the signing key when upgrading from those versions too.

Read the session in a handler

The watchdog validates the session cookie before forwarding each request, but the handler should still verify it before trusting its claims. Template servers listen on all interfaces inside the function's Pod, so anything that can reach the Pod directly can bypass the watchdog and send a forged cookie.

Verify the cookie with the same signing key the watchdog uses. Mount the secret, then check the following:

  • the HS256 signature
  • the issuer and audience, which must both equal oauth_base_url
  • the expiry and issued-at time
  • the typ claim, which must equal session

The oauth_base_url environment variable is available to the handler as well as the watchdog.

For Go, using golang-jwt:

type Session struct {
    jwt.RegisteredClaims
    Type            string   `json:"typ"`
    FederatedIssuer string   `json:"fed:iss,omitempty"`
    FederatedEmail  string   `json:"fed:email,omitempty"`
    EmailVerified   *bool    `json:"fed:email_verified,omitempty"`
    FederatedName   string   `json:"fed:name,omitempty"`
    FederatedGroups []string `json:"fed:groups,omitempty"`
    GroupsTruncated bool     `json:"fed:groups_truncated,omitempty"`
}

var (
    baseURL    = os.Getenv("oauth_base_url")
    signingKey = mustReadKey("/var/openfaas/secrets/profile-signing-key")
)

func mustReadKey(path string) []byte {
    data, err := os.ReadFile(path)
    if err != nil {
        panic(err)
    }
    key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(data)))
    if err != nil {
        panic(err)
    }
    return key
}

// readSession verifies the cookie with the function's signing key.
func readSession(r *http.Request) (*Session, error) {
    cookies := r.CookiesNamed("of_session")
    if len(cookies) != 1 || cookies[0].Value == "" {
        return nil, errors.New("missing or duplicate session cookie")
    }
    session := &Session{}
    _, err := jwt.ParseWithClaims(cookies[0].Value, session,
        func(*jwt.Token) (any, error) { return signingKey, nil },
        jwt.WithValidMethods([]string{"HS256"}),
        jwt.WithIssuer(baseURL),
        jwt.WithAudience(baseURL),
        jwt.WithExpirationRequired(),
        jwt.WithIssuedAt(),
        jwt.WithStrictDecoding())
    if err != nil || session.IssuedAt == nil || session.Type != "session" ||
        len(session.Audience) != 1 || session.Audience[0] != baseURL {
        return nil, errors.New("invalid session")
    }
    return session, nil
}

For Python, using PyJWT:

import base64
import os
from http.cookies import CookieError, SimpleCookie

import jwt

BASE_URL = os.environ["oauth_base_url"]

with open("/var/openfaas/secrets/profile-signing-key") as f:
    SIGNING_KEY = base64.b64decode(f.read().strip())


def read_session(cookie_header):
    """Verify the watchdog's of_session cookie and return its claims."""
    values = []
    try:
        for part in (cookie_header or "").split(";"):
            parsed = SimpleCookie()
            parsed.load(part)
            cookie = parsed.get("of_session")
            if cookie is not None:
                values.append(cookie.value)
    except CookieError:
        return None
    if len(values) != 1 or not values[0]:
        return None
    try:
        claims = jwt.decode(
            values[0],
            SIGNING_KEY,
            algorithms=["HS256"],
            issuer=BASE_URL,
            audience=BASE_URL,
            options={"require": ["exp", "iat", "iss", "aud", "typ"]},
        )
    except jwt.InvalidTokenError:
        return None
    if claims.get("typ") != "session" or claims.get("aud") != [BASE_URL]:
        return None
    return claims

With the python3-http template, pass event.headers.get("Cookie") to read_session. If oauth_cookie_name is set, use that name instead of of_session.

Base authorization decisions on sub, not on fed:email or fed:name, which the provider may allow users to change. If group membership is used, prefer an oauth_groups selection containing only the groups the handler needs, and account for fed:groups_truncated. Do not log the raw cookie or return it to browser code.