Authentication Tutorial#
This tutorial walks you through setting up and using authentication in scafctl. You’ll learn how to authenticate with Microsoft Entra ID, GitHub, and Google Cloud Platform, manage credentials, and use authenticated HTTP requests in your solutions.
Prerequisites#
- scafctl installed and available in your PATH
- For Entra: Access to a Microsoft Entra ID tenant
- For GitHub: A GitHub account (or GitHub Enterprise Server instance)
- For GCP: A Google Cloud account
- A web browser for completing the device code flow
Table of Contents#
- Understanding Auth in scafctl
- Logging In
- Entra Device Code Flow
- Entra Service Principal
- Entra Workload Identity
- GitHub Device Code Flow
- GitHub PAT Flow
- GitHub Interactive Flow (Browser OAuth + PKCE)
- GitHub App Installation Token
- GCP Interactive Login
- GCP Service Account Key
- GCP Workload Identity Federation
- GCP Metadata Server
- Host-Aware Login: Hostname Aliases and Dynamic Resolution
- Checking Auth Status
- Listing and Sorting Cached Tokens
- Using Auth in HTTP Providers
- Getting Tokens for Debugging
- Configuration
- Logging Out
- Auth Diagnostics
- Troubleshooting
Understanding Auth in scafctl#
Authentication in scafctl follows these principles:
- Providers declare auth requirements, not credentials
- Token acquisition is separated from provider execution
- Refresh tokens are stored securely using your system’s secret store
- Access tokens are short-lived and cached for performance
- Secrets never appear in solution files or logs
- Auth tokens are visible via
scafctl secrets list --allandscafctl secrets get <name> --all
scafctl currently supports the following auth handlers:
| Handler | Description | Flows |
|---|---|---|
entra | Microsoft Entra ID (Azure AD) | Interactive (Browser OAuth + PKCE), Device Code, Service Principal, Workload Identity |
github | GitHub (github.com and GHES) | Interactive (Browser OAuth + PKCE), Device Code, PAT (Personal Access Token), GitHub App |
gcp | Google Cloud Platform | Interactive (Browser OAuth), Service Account Key, Workload Identity Federation, Metadata Server |
You can always discover registered handlers and their capabilities at runtime:
# List all handlers with flows and capabilities
scafctl auth list
# Output as JSON for scripting
scafctl auth list -o json# List all handlers with flows and capabilities
scafctl auth list
# Output as JSON for scripting
scafctl auth list -o jsonLogging In#
To authenticate with Microsoft Entra ID, use the auth login command:
scafctl auth login entrascafctl auth login entraBy default, this opens your browser for an OAuth authorization code flow with PKCE – the same approach used by az login, gh auth login, and gcloud auth login:
- scafctl starts a local HTTP server on an ephemeral port
- Your browser opens to the Microsoft login page
- Sign in and grant consent
- The browser redirects back to the local server with an authorization code
- scafctl exchanges the code for tokens and stores your refresh token securely
Example Output#
→ Opening browser for authentication...
If the browser does not open, visit: https://login.microsoftonline.com/...
✓ Successfully authenticated as user@example.com
Tenant: contoso.onmicrosoft.comDevice Code Flow (Headless / SSH Fallback)#
If you are in a headless environment, over SSH, or the browser cannot open, use the device code flow:
scafctl auth login entra --flow device-codescafctl auth login entra --flow device-codeThis displays a code and URL for you to enter manually:
To sign in, use a web browser to open the page https://microsoft.com/devicelogin
and enter the code ABCD1234 to authenticate.
Waiting for authentication...
✓ Successfully authenticated as user@example.com
Tenant: contoso.onmicrosoft.comIdempotent Login for Scripts (–skip-if-authenticated)#
Use --skip-if-authenticated to skip re-authentication if you’re already logged in. The command exits 0 without prompting, making it safe to call at the start of scripts or CI jobs without disrupting an active session:
# Only login if not already authenticated
scafctl auth login entra --skip-if-authenticated
scafctl auth login github --skip-if-authenticated
scafctl auth login gcp --skip-if-authenticated# Only login if not already authenticated
scafctl auth login entra --skip-if-authenticated
scafctl auth login github --skip-if-authenticated
scafctl auth login gcp --skip-if-authenticatedWhen already authenticated this prints a message and exits 0. Without the flag, the command prompts for re-authentication (or warns if already authenticated but continues).
Specifying a Tenant#
By default, scafctl uses the “organizations” tenant (any work/school account). To authenticate with a specific tenant:
# Use a specific tenant ID
scafctl auth login entra --tenant 08e70e8e-d05c-4449-a2c2-67bd0a9c4e79
# Use a tenant domain
scafctl auth login entra --tenant contoso.onmicrosoft.com# Use a specific tenant ID
scafctl auth login entra --tenant 08e70e8e-d05c-4449-a2c2-67bd0a9c4e79
# Use a tenant domain
scafctl auth login entra --tenant contoso.onmicrosoft.comCustom Client ID#
By default, scafctl uses the Azure CLI’s public client ID (04b07795-8ddb-461a-bbee-02f9e1bf7b46). If your organization requires a custom app registration (e.g., for specific permissions or conditional access policies), use the --client-id flag:
scafctl auth login entra --client-id 12345678-abcd-1234-abcd-123456789abcscafctl auth login entra --client-id 12345678-abcd-1234-abcd-123456789abcThe client ID used during login is persisted in your credential metadata so that subsequent token refreshes use the same client ID, even if your configuration file specifies a different one. This prevents token minting failures caused by a mismatch between the login client ID and the refresh client ID.
You can also set a default client ID via the scafctl configuration file under auth.entra.clientId. Note that the --client-id flag at login time always takes precedence, and the stored client ID from login will be used for all future token refreshes.
Important – Redirect URI registration: When using a custom client ID with the interactive (browser) login flow, the app registration must have
http://localhostregistered as a redirect URI. Without it, Entra returns AADSTS500113 and the CLI times out. In the Azure portal, go to App registrations → your app → Authentication → Add a platform → Mobile and desktop applications and addhttp://localhost.If you cannot modify the app registration, use
--flow device-codeinstead (device code does not require a redirect URI), or use--callback-portto bind the callback server to a specific port that matches a registered redirect URI:# If the app registration has http://localhost:8400 as a redirect URI scafctl auth login entra --client-id 12345678-abcd-1234-abcd-123456789abc --callback-port 8400
Setting a Callback Port#
By default, the interactive login flow starts a local HTTP server on a random
(ephemeral) port. Some app registrations only allow specific redirect URIs. Use
--callback-port to bind to a predictable port:
scafctl auth login entra --callback-port 8400scafctl auth login entra --callback-port 8400This makes the redirect URI http://localhost:8400, which must be registered in
the app registration’s Authentication settings.
--callback-portmust be an unprivileged, in-range TCP port (1024-65535); any other value is rejected. It works for both built-in handlers and plugin auth handlers that advertise thecallback_portcapability.
Setting a Timeout#
The interactive login flow has a 5-minute default timeout. To extend it:
scafctl auth login entra --timeout 10mscafctl auth login entra --timeout 10mRequesting Specific Scopes#
By default, login requests only basic scopes (openid, profile, offline_access). If your resolvers need access to specific APIs (e.g., Microsoft Graph), include the required scope during login to establish consent:
# Login with Microsoft Graph scope
scafctl auth login entra --scope https://graph.microsoft.com/.default
# Login with Azure Resource Manager scope
scafctl auth login entra --scope https://management.azure.com/.default# Login with Microsoft Graph scope
scafctl auth login entra --scope https://graph.microsoft.com/.default
# Login with Azure Resource Manager scope
scafctl auth login entra --scope https://management.azure.com/.defaultThis ensures your authentication session has consent for that API resource, preventing “consent required” errors when resolvers run. The refresh token obtained at login can then be used to mint access tokens for the consented resource.
Note: Login should target a single API resource at a time. If you need tokens for multiple API resources, use separate
scafctl auth logincalls, or rely on the refresh token to mint tokens for additional resources at runtime viascafctl auth token.
Service Principal Authentication (CI/CD)#
For non-interactive scenarios like CI/CD pipelines, use service principal authentication:
# Set credentials in environment variables
export AZURE_CLIENT_ID="your-app-client-id"
export AZURE_TENANT_ID="your-tenant-id"
export AZURE_CLIENT_SECRET="your-client-secret"
# Login with service principal (auto-detected from env vars)
scafctl auth login entra
# Or explicitly specify the flow
scafctl auth login entra --flow service-principal# Set credentials in environment variables
$env:AZURE_CLIENT_ID = "your-app-client-id"
$env:AZURE_TENANT_ID = "your-tenant-id"
$env:AZURE_CLIENT_SECRET = "your-client-secret"
# Login with service principal (auto-detected from env vars)
scafctl auth login entra
# Or explicitly specify the flow
scafctl auth login entra --flow service-principalEnvironment Variables:
| Variable | Description |
|---|---|
AZURE_CLIENT_ID | Application (client) ID of the service principal |
AZURE_TENANT_ID | Directory (tenant) ID |
AZURE_CLIENT_SECRET | Client secret value |
Note: When AZURE_CLIENT_SECRET is set, scafctl automatically uses the service principal flow.
Workload Identity Authentication (Kubernetes)#
Workload Identity enables secretless authentication for workloads running on Kubernetes. This is the recommended approach for AKS and other Kubernetes environments as it eliminates the need to manage secrets.
Quick Start (In-Cluster)#
When running inside a properly configured AKS pod, the Azure Workload Identity webhook automatically injects the required environment variables:
# Auto-detected when running in a configured pod
scafctl auth login entra
# Or explicitly specify the flow
scafctl auth login entra --flow workload-identity# Auto-detected when running in a configured pod
scafctl auth login entra
# Or explicitly specify the flow
scafctl auth login entra --flow workload-identityEnvironment Variables (auto-injected by webhook):
| Variable | Description |
|---|---|
AZURE_CLIENT_ID | Client ID of the managed identity or app registration |
AZURE_TENANT_ID | Directory (tenant) ID |
AZURE_FEDERATED_TOKEN_FILE | Path to the projected service account token |
AZURE_AUTHORITY_HOST | (Optional) Azure AD authority URL |
Testing Workload Identity Locally#
For development and testing outside of AKS, you can manually configure workload identity federation. This involves:
- Creating an Entra App Registration with federated credentials
- Generating a Kubernetes service account token
- Using the token with scafctl
Step 1: Create an Entra App Registration#
- Register a new application in the Azure Portal :
# Using Azure CLI
az ad app create --display-name "scafctl-workload-identity-test" # Using Azure CLI
az ad app create --display-name "scafctl-workload-identity-test"Note the Application (client) ID - you’ll need this for
AZURE_CLIENT_IDCreate a service principal for the application:
az ad sp create --id <application-id>Grant API permissions as needed (e.g., Microsoft Graph, Azure Resource Manager)
Step 2: Configure Federated Identity Credential#
The federated identity credential tells Entra ID to trust tokens from your Kubernetes cluster’s OIDC issuer.
Get your Kubernetes cluster’s OIDC issuer URL:
# For AKS az aks show --name <cluster-name> --resource-group <rg-name> \ --query "oidcIssuerProfile.issuerUrl" -o tsv # For other clusters (e.g., kind, minikube with OIDC enabled) kubectl get --raw /.well-known/openid-configuration | jq -r '.issuer'az aks show --name <cluster-name> --resource-group <rg-name> ` --query "oidcIssuerProfile.issuerUrl" -o tsv # For other clusters (kubectl get --raw /.well-known/openid-configuration | ConvertFrom-Json).issuerCreate the federated credential via Azure Portal or CLI:
# Using Azure CLI az ad app federated-credential create --id <application-id> --parameters '{ "name": "kubernetes-federated-credential", "issuer": "<your-oidc-issuer-url>", "subject": "system:serviceaccount:<namespace>:<service-account-name>", "audiences": ["api://AzureADTokenExchange"] }'Important fields:
issuer: Your Kubernetes cluster’s OIDC issuer URLsubject: Must match the service account in formatsystem:serviceaccount:<namespace>:<name>audiences: Must be["api://AzureADTokenExchange"]for Azure workload identity
Example for a service account named
scafctl-sain thedefaultnamespace:
az ad app federated-credential create --id <application-id> --parameters '{
"name": "scafctl-test-credential",
"issuer": "https://oidc.example.com/abc123",
"subject": "system:serviceaccount:default:scafctl-sa",
"audiences": ["api://AzureADTokenExchange"]
}' az ad app federated-credential create --id <application-id> --parameters '{
"name": "scafctl-test-credential",
"issuer": "https://oidc.example.com/abc123",
"subject": "system:serviceaccount:default:scafctl-sa",
"audiences": ["api://AzureADTokenExchange"]
}'Step 3: Create a Kubernetes Service Account and Generate Token#
Create a service account (if it doesn’t exist):
# service-account.yaml apiVersion: v1 kind: ServiceAccount metadata: name: scafctl-sa namespace: defaultkubectl apply -f service-account.yamlGenerate a token using
kubectl create token:
# Generate a token with the correct audience
kubectl create token scafctl-sa \
--namespace default \
--audience "api://AzureADTokenExchange" \
--duration 1h # Generate a token with the correct audience
kubectl create token scafctl-sa `
--namespace default `
--audience "api://AzureADTokenExchange" `
--duration 1hImportant: The --audience must match what you configured in the federated credential.
- Save the token to an environment variable or file:
# Save to environment variable
export FEDERATED_TOKEN=$(kubectl create token scafctl-sa \
--namespace default \
--audience "api://AzureADTokenExchange" \
--duration 1h)
# Or save to a file
kubectl create token scafctl-sa \
--namespace default \
--audience "api://AzureADTokenExchange" \
--duration 1h > /tmp/federated-token.txt # Save to environment variable
$env:FEDERATED_TOKEN = $(kubectl create token scafctl-sa `
--namespace default `
--audience "api://AzureADTokenExchange" `
--duration 1h)
# Or save to a file
kubectl create token scafctl-sa `
--namespace default `
--audience "api://AzureADTokenExchange" `
--duration 1h > /tmp/federated-token.txtStep 4: Authenticate with scafctl#
Option A: Using the --federated-token flag (recommended for testing):
export AZURE_CLIENT_ID="<your-application-client-id>"
export AZURE_TENANT_ID="<your-tenant-id>"
# Pass the token directly
scafctl auth login entra --flow workload-identity --federated-token "$FEDERATED_TOKEN"$env:AZURE_CLIENT_ID = "<your-application-client-id>"
$env:AZURE_TENANT_ID = "<your-tenant-id>"
# Pass the token directly
scafctl auth login entra --flow workload-identity --federated-token "$FEDERATED_TOKEN"Option B: Using the AZURE_FEDERATED_TOKEN environment variable:
export AZURE_CLIENT_ID="<your-application-client-id>"
export AZURE_TENANT_ID="<your-tenant-id>"
export AZURE_FEDERATED_TOKEN="$FEDERATED_TOKEN"
scafctl auth login entra --flow workload-identity$env:AZURE_CLIENT_ID = "<your-application-client-id>"
$env:AZURE_TENANT_ID = "<your-tenant-id>"
$env:AZURE_FEDERATED_TOKEN = "$FEDERATED_TOKEN"
scafctl auth login entra --flow workload-identityOption C: Using a token file (simulates in-cluster behavior):
export AZURE_CLIENT_ID="<your-application-client-id>"
export AZURE_TENANT_ID="<your-tenant-id>"
export AZURE_FEDERATED_TOKEN_FILE="/tmp/federated-token.txt"
scafctl auth login entra --flow workload-identity$env:AZURE_CLIENT_ID = "<your-application-client-id>"
$env:AZURE_TENANT_ID = "<your-tenant-id>"
$env:AZURE_FEDERATED_TOKEN_FILE = "/tmp/federated-token.txt"
scafctl auth login entra --flow workload-identityStep 5: Verify Authentication#
# Check auth status
scafctl auth status entra
# Output for workload identity:
# Handler Status Identity IdentityType TokenFile
# entra Authenticated Workload Identity (12345678...) workload-identity (direct token)# Check auth status
scafctl auth status entra
# Output for workload identity:
# Handler Status Identity IdentityType TokenFile
# entra Authenticated Workload Identity (12345678...) workload-identity (direct token)Complete Example Script#
Here’s a complete script for testing workload identity locally:
#!/bin/bash
set -e
# Configuration - update these values
APP_CLIENT_ID="12345678-1234-1234-1234-123456789012"
TENANT_ID="your-tenant-id"
NAMESPACE="default"
SERVICE_ACCOUNT="scafctl-sa"
# Generate a fresh token
echo "Generating service account token..."
FEDERATED_TOKEN=$(kubectl create token "$SERVICE_ACCOUNT" \
--namespace "$NAMESPACE" \
--audience "api://AzureADTokenExchange" \
--duration 1h)
# Set environment variables
export AZURE_CLIENT_ID="$APP_CLIENT_ID"
export AZURE_TENANT_ID="$TENANT_ID"
# Authenticate
echo "Authenticating with workload identity..."
scafctl auth login entra --flow workload-identity --federated-token "$FEDERATED_TOKEN"
# Verify
echo ""
echo "Authentication status:"
scafctl auth status entra
# Test token acquisition
echo ""
echo "Getting access token for Azure Resource Manager..."
scafctl auth token entra --scope "https://management.azure.com/.default"$ErrorActionPreference = "Stop"
# Configuration - update these values
$APP_CLIENT_ID = "12345678-1234-1234-1234-123456789012"
$TENANT_ID = "your-tenant-id"
$NAMESPACE = "default"
$SERVICE_ACCOUNT = "scafctl-sa"
# Generate a fresh token
Write-Output "Generating service account token..."
$FEDERATED_TOKEN = $(kubectl create token $SERVICE_ACCOUNT `
--namespace $NAMESPACE `
--audience "api://AzureADTokenExchange" `
--duration 1h)
# Set environment variables
$env:AZURE_CLIENT_ID = $APP_CLIENT_ID
$env:AZURE_TENANT_ID = $TENANT_ID
# Authenticate
Write-Output "Authenticating with workload identity..."
scafctl auth login entra --flow workload-identity --federated-token $FEDERATED_TOKEN
# Verify
Write-Output ""
Write-Output "Authentication status:"
scafctl auth status entra
# Test token acquisition
Write-Output ""
Write-Output "Getting access token for Azure Resource Manager..."
scafctl auth token entra --scope "https://management.azure.com/.default"Flow Priority and Interaction with Stored Credentials#
The Entra handler selects which flow to use based on what is available at runtime, in this order:
| Priority | Flow | What triggers it |
|---|---|---|
| 1 (highest) | Workload Identity | AZURE_FEDERATED_TOKEN_FILE or AZURE_FEDERATED_TOKEN is set |
| 2 | Service Principal | AZURE_CLIENT_SECRET is set |
| 3 (lowest) | Device Code / Refresh Token | A refresh token is stored in the system secret store |
WIF does not touch the stored refresh token.
When WIF is active, the stored device-code refresh token (if any) is bypassed but not deleted. The two credential types live in completely separate storage:
- WIF is entirely env-var driven – no reads or writes to
scafctl.auth.entra.refresh_token - A prior device-code session silently coexists in the secret store while WIF is active
scafctl auth listwill display both the WIF-sourced access tokens and any stored refresh token
Fallback behavior: removing WIF env vars causes the handler to automatically fall through to the next available flow. If you have a stored refresh token, it resumes being used with no reconfiguration needed.
Stale stored credentials: if you want a clean slate after switching to WIF, explicitly remove the stored device-code session:
scafctl auth logout entrascafctl auth logout entraThis removes the refresh token and cached access tokens without affecting WIF, which is entirely driven by environment variables.
Troubleshooting Workload Identity#
Error: “AADSTS70021: No matching federated identity record found”
This means the token’s claims don’t match the federated credential configuration:
- Verify the
issuermatches your cluster’s OIDC issuer URL exactly - Verify the
subjectmatches your service account (system:serviceaccount:<namespace>:<name>) - Verify the
audiencein both the federated credential andkubectl create tokencommand
# Decode the token to inspect its claims
echo "$FEDERATED_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .# Decode the token to inspect its claims
$parts = $env:FEDERATED_TOKEN -split '\.'
[System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($parts[1] + '=' * (4 - $parts[1].Length % 4))) | ConvertFrom-JsonCheck these claims match your federated credential:
iss(issuer)sub(subject)aud(audience)
Error: “AADSTS700024: Client assertion is not within its valid time range”
The token has expired. Generate a new one:
kubectl create token scafctl-sa --audience "api://AzureADTokenExchange" --duration 1hkubectl create token scafctl-sa --audience "api://AzureADTokenExchange" --duration 1hError: “workload identity not configured”
Ensure all required environment variables are set:
echo "AZURE_CLIENT_ID: $AZURE_CLIENT_ID"
echo "AZURE_TENANT_ID: $AZURE_TENANT_ID"
echo "AZURE_FEDERATED_TOKEN: ${AZURE_FEDERATED_TOKEN:0:20}..." # First 20 charsChecking the OIDC Discovery Document
Verify your cluster’s OIDC configuration is accessible:
# Get the OIDC configuration
curl -s "$(kubectl get --raw /.well-known/openid-configuration | jq -r '.issuer')/.well-known/openid-configuration" | jq .
# Get the JWKS (signing keys)
curl -s "$(kubectl get --raw /.well-known/openid-configuration | jq -r '.jwks_uri')" | jq .$issuer = (kubectl get --raw /.well-known/openid-configuration | ConvertFrom-Json).issuer
Invoke-RestMethod "$issuer/.well-known/openid-configuration"Host-Aware Login: Hostname Aliases and Dynamic Resolution#
Handlers that advertise the hostname capability accept a --hostname <selector>
argument at login. Instead of memorizing a long API server URL, you log in with a
short selector (e.g. prod) and the scafctl host resolves it into a concrete
endpoint URL before delegating to the handler.
Resolution is host-side and shape-blind: the core never hardcodes any
organization’s inventory format. For a given --hostname <selector>, the host
resolves in this order (first match wins):
- Concrete URL – if the selector already parses as an
http/httpsURL, it is forwarded unchanged. - Static alias – a
hostname.aliasesentry mapping selector to URL. - Dynamic resolver –
hostname.resolverfetches an inventory and normalizes it into{name, url}entries; the selector is matched byname. - Otherwise, login fails and lists the available selectors.
Static aliases#
Manage aliases with the auth alias command. They are written to your
config.yaml under auth.handlers.<handler>.hostname.aliases:
scafctl auth alias set openshift prod https://api.prod.example.com:6443
scafctl auth alias set openshift stg https://api.stg.example.com:6443
scafctl auth alias list openshift
scafctl auth alias remove openshift stg
scafctl auth login openshift --hostname prodEquivalent config:
auth:
handlers:
openshift:
hostname:
aliases:
prod: https://api.prod.example.com:6443
stg: https://api.stg.example.com:6443Only handlers that declare the hostname capability accept aliases; auth alias set
fails fast otherwise.
Dynamic resolution#
When a fleet has too many endpoints to alias by hand, configure a resolver. At
login time the host performs an HTTPS GET, runs a CEL transform to normalize the
response into a list of {name, url} entries, and caches the result for the
configured TTL:
auth:
handlers:
openshift:
hostname:
resolver:
source:
url: https://clusters.example.com/inventory
# Optional: inject a bearer token from another auth handler.
authProvider: entra
authScope: api://fleet-inventory/.default
# CEL transform: normalize the fetched JSON (bound to `_`) into a
# list of {name, url} objects. Filter out anything not usable.
transform: |
_.map(k, _[k].status != "deleted", {
"name": k,
"url": _[k].apiServerURL,
})
ttl: 1hThe transform must return a list of objects, each with a non-empty name and
url; any other shape fails with a clear “transform shape” error. Entries may
also carry optional per-cluster OIDC metadata (audience, authType, caData,
consoleUrl, insecureSkipTls) so a single inventory can drive both
auth login --hostname and kube login.
For the full transform contract, caching behavior, authenticated inventory endpoints, and troubleshooting, see the hostname resolution example and the hostname resolution design doc .
Multi-Cluster Token Routing#
kube login can wire up several clusters that share one auth handler. Each
kube login writes a kubeconfig exec entry with provideClusterInfo: true, so
kubectl/oc pass the target cluster’s API server URL to the exec helper on
every credential call. The helper (auth token --exec-credential) forwards that
URL to the handler, which returns the token for that specific cluster – so two
contexts backed by the same handler each mint the correct token with no
re-login.
This routing is active only for handlers that advertise the token_hostname
capability (auth.CapTokenHostname). If a handler does not – for example an
older handler built before SDK v0.14.0, or one that only advertises the login
hostname capability – kube login prints a warning that logging into
multiple clusters with it may return the wrong token; rebuild the handler
against SDK v0.14.0 and advertise token_hostname to enable per-cluster
routing.
To request a specific cluster’s token directly (for scripting or debugging,
without kubectl setting KUBERNETES_EXEC_INFO), pass the cluster’s API server
URL to auth token --server:
scafctl auth token openshift --server https://api.a.example.com:6443 --raw
--server takes precedence over KUBERNETES_EXEC_INFO and is honored only for
handlers that advertise token_hostname. The structured kube login output
(-o json) also reports a perClusterTokens boolean so scripts can detect
whether the resolved handler supports this routing.
GitHub Interactive Flow (Browser OAuth + PKCE)#
The default GitHub login flow opens your browser for OAuth Authorization Code + PKCE authentication – the same approach used by gh auth login and the Entra handler:
scafctl auth login githubscafctl auth login githubThis initiates a browser-based login:
- scafctl starts a local HTTP server on an ephemeral port
- Your browser opens to the GitHub authorization page
- Authorize the scafctl OAuth App
- GitHub redirects back to the local server with an authorization code
- scafctl exchanges the code for tokens using PKCE verification
- Credentials are stored securely
Example Output#
→ Opening browser for authentication...
If the browser does not open, visit: https://github.com/login/oauth/authorize?...
✓ Authentication successful!
Name: The Octocat
Username: octocat
Email: octocat@github.com
Flow: InteractiveSetting a Callback Port#
By default, the interactive flow starts a local HTTP server on a random (ephemeral) port. Use --callback-port to bind to a predictable port:
scafctl auth login github --callback-port 8400scafctl auth login github --callback-port 8400GitHub Device Code Flow#
For headless environments, SSH sessions, or when the browser cannot open, use the device code flow:
scafctl auth login github --flow device-codescafctl auth login github --flow device-code- scafctl displays a code and URL
- Open the URL in your browser
- Enter the code when prompted
- Authorize the scafctl OAuth App
- scafctl stores your credentials securely
Example Output#
To sign in, use a web browser to open the page:
https://github.com/login/device
Enter the code: ABCD-1234
Waiting for authentication...
✓ Authentication successful!
Name: The Octocat
Username: octocat
Email: octocat@github.com
Flow: Device CodeGitHub Enterprise Server#
To authenticate with a GitHub Enterprise Server instance:
scafctl auth login github --hostname github.example.comscafctl auth login github --hostname github.example.comThis adjusts all OAuth and API endpoints to use your GHES instance.
Custom Client ID#
By default, scafctl uses its own OAuth App client ID (Ov23li6xn492GhPmt4YG). If your organization requires a custom OAuth App:
scafctl auth login github --client-id your-custom-client-idscafctl auth login github --client-id your-custom-client-idRequesting Specific Scopes#
By default, login requests gist, read:org, repo, and workflow scopes (matching the gh CLI). To request different scopes:
# Login with additional scopes
scafctl auth login github --scope repo --scope read:org --scope write:packages
# Or comma-separated
scafctl auth login github --scope "repo,read:org,write:packages"# Login with additional scopes
scafctl auth login github --scope repo --scope read:org --scope write:packages
# Or comma-separated
scafctl auth login github --scope "repo,read:org,write:packages"Important: GitHub scopes are fixed at login time. Unlike Entra ID, GitHub’s OAuth token refresh does not support changing scopes per-request. The
--scopeflag onscafctl auth token githubis not supported. If you need different scopes, you must log out and log in again with the desired scopes.
Setting a Timeout#
The device code flow has a 5-minute default timeout. To extend it:
scafctl auth login github --timeout 10mscafctl auth login github --timeout 10mGitHub PAT Authentication (CI/CD)#
For non-interactive scenarios like CI/CD pipelines, use a Personal Access Token:
# Set token in environment (GITHUB_TOKEN takes precedence)
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Login with PAT (auto-detected from env vars)
scafctl auth login github
# Or explicitly specify the flow
scafctl auth login github --flow pat# Set token in environment (GITHUB_TOKEN takes precedence)
$env:GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Login with PAT (auto-detected from env vars)
scafctl auth login github
# Or explicitly specify the flow
scafctl auth login github --flow patEnvironment Variables:
| Variable | Description | Priority |
|---|---|---|
GITHUB_TOKEN | GitHub personal access token or Actions token | 1 (highest) |
GH_TOKEN | GitHub personal access token (gh CLI convention) | 2 |
GH_HOST | GitHub hostname for Enterprise Server | – |
Notes:
- In GitHub Actions,
GITHUB_TOKENis automatically injected - When either token env var is set, scafctl automatically uses the PAT flow
- PATs don’t have a defined expiry, so status shows as authenticated until the token is revoked
- The PAT identity type shows as
service-principalsince it acts like a non-interactive credential
GitHub App Installation Token#
For service-to-service automation and CI/CD without user tokens, use a GitHub App installation token. This is the recommended approach for automated workflows that need repository access.
Prerequisites#
- Create a GitHub App in your organization or account
- Generate a private key for the App
- Install the App on the target repository/organization
- Note the App ID and Installation ID
Configuration#
Set the GitHub App credentials via config file, environment variables, or a combination:
Config file (~/.config/scafctl/config.yaml):
auth:
github:
appId: "12345"
installationId: "67890"
privateKeyPath: "/path/to/private-key.pem"Environment variables:
export SCAFCTL_GITHUB_APP_ID="12345"
export SCAFCTL_GITHUB_APP_INSTALLATION_ID="67890"
export SCAFCTL_GITHUB_APP_PRIVATE_KEY_PATH="/path/to/private-key.pem"Login#
# Login with GitHub App (requires App credentials configured)
scafctl auth login github --flow github-app# Login with GitHub App (requires App credentials configured)
scafctl auth login github --flow github-appExample Output#
✓ Authentication successful!
App: my-automation-app
Installation ID: 67890
Flow: GitHub App
Identity Type: service-principalPrivate Key Sources#
The private key can be provided from multiple sources (checked in priority order):
| Source | Config Field | Env Var | Security |
|---|---|---|---|
| Inline PEM | privateKey | SCAFCTL_GITHUB_APP_PRIVATE_KEY | ⚠️ Least secure – key visible in config/env |
| File path | privateKeyPath | SCAFCTL_GITHUB_APP_PRIVATE_KEY_PATH | ✅ Good – key in a file with restricted permissions |
| Secret store | privateKeySecretName | – | ✅ Best – key encrypted by OS keychain |
Security recommendation: Prefer
privateKeySecretName(encrypted secret store) orprivateKeyPath(file withchmod 600) over inlineprivateKey. When the inline method is used, scafctl logs a warning recommending a more secure alternative.
Inline PEM (CI/CD secret):
export SCAFCTL_GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
scafctl auth login github --flow github-app$env:SCAFCTL_GITHUB_APP_PRIVATE_KEY = ""-----BEGIN"
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
scafctl auth login github --flow github-appFile path:
export SCAFCTL_GITHUB_APP_PRIVATE_KEY_PATH="/path/to/private-key.pem"
scafctl auth login github --flow github-app$env:SCAFCTL_GITHUB_APP_PRIVATE_KEY_PATH = "/path/to/private-key.pem"
scafctl auth login github --flow github-appSecret store:
# config.yaml
auth:
github:
appId: "12345"
installationId: "67890"
privateKeySecretName: "github-app-private-key"GitHub Enterprise Server#
The GitHub App flow works with GHES by setting --hostname:
scafctl auth login github --flow github-app --hostname github.example.comscafctl auth login github --flow github-app --hostname github.example.comNotes#
- Installation tokens expire after 1 hour and are automatically cached
- The identity type is
service-principal(no user context) - The App must be installed on the target organization/repository
- Both PKCS#1 (
BEGIN RSA PRIVATE KEY) and PKCS#8 (BEGIN PRIVATE KEY) PEM formats are supported
GCP Interactive Login (Browser OAuth)#
For local development, use the interactive browser OAuth flow:
# Login with GCP using browser OAuth (default -- no gcloud required)
scafctl auth login gcp
# Login with specific scopes
scafctl auth login gcp --scope https://www.googleapis.com/auth/bigquery
# Login with service account impersonation
scafctl auth login gcp --impersonate-service-account my-sa@my-project.iam.gserviceaccount.com
# Login with a custom OAuth client ID (overrides the built-in default)
scafctl auth login gcp --client-id YOUR_CLIENT_ID# Login with GCP using browser OAuth (default -- no gcloud required)
scafctl auth login gcp
# Login with specific scopes
scafctl auth login gcp --scope https://www.googleapis.com/auth/bigquery
# Login with service account impersonation
scafctl auth login gcp --impersonate-service-account my-sa@my-project.iam.gserviceaccount.com
# Login with a custom OAuth client ID (overrides the built-in default)
scafctl auth login gcp --client-id YOUR_CLIENT_IDThis will:
- Start a local HTTP server
- Open your browser to Google’s OAuth consent page
- Exchange the authorization code for refresh + access tokens
- Store the refresh token in your system’s secret store
Note: scafctl uses Google’s well-known ADC client credentials by default – the same ones used by
gcloud auth application-default login. No gcloud installation is required. To use a custom OAuth client, see GCP Custom OAuth Client Setup .
GCP gcloud ADC Fallback#
If you already have gcloud configured and prefer to use its existing credentials:
# Use existing gcloud Application Default Credentials
scafctl auth login gcp --flow gcloud-adc# Use existing gcloud Application Default Credentials
scafctl auth login gcp --flow gcloud-adcThis reads the refresh token from ~/.config/gcloud/application_default_credentials.json (produced by gcloud auth application-default login). Note that this flow is subject to your organization’s RAPT re-authentication policies.
GCP Service Account Key Authentication (CI/CD)#
For non-interactive scenarios, use a service account key file:
# Point to your service account key JSON file
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/sa-key.json"
# Login with service account (auto-detected from env var)
scafctl auth login gcp
# Or choose the flow explicitly
scafctl auth login gcp --flow service-principal# Point to your service account key JSON file
$env:GOOGLE_APPLICATION_CREDENTIALS = "/path/to/sa-key.json"
# Login with service account (auto-detected from env var)
scafctl auth login gcp
# Or choose the flow explicitly
scafctl auth login gcp --flow service-principalGCP Workload Identity Federation#
For Kubernetes and other cloud platforms:
# Point to external account JSON config
export GOOGLE_EXTERNAL_ACCOUNT="/path/to/external-account.json"
# Login (auto-detected)
scafctl auth login gcp
# Or explicitly
scafctl auth login gcp --flow workload-identity# Point to external account JSON config
$env:GOOGLE_EXTERNAL_ACCOUNT = "/path/to/external-account.json"
# Login (auto-detected)
scafctl auth login gcp
# Or explicitly
scafctl auth login gcp --flow workload-identityGCP Metadata Server (GCE/GKE/Cloud Run)#
On Google Compute Engine, GKE, and Cloud Run:
# Login via metadata server (auto-detected on GCE)
scafctl auth login gcp --flow metadata# Login via metadata server (auto-detected on GCE)
scafctl auth login gcp --flow metadataChecking Auth Status#
To see your current authentication status:
# Show status for all handlers
scafctl auth status
# Show status for a specific handler
scafctl auth status entra
# Show GitHub auth status
scafctl auth status github
# Output as JSON
scafctl auth status -o json# Show status for all handlers
scafctl auth status
# Show status for a specific handler
scafctl auth status entra
# Show GitHub auth status
scafctl auth status github
# Output as JSON
scafctl auth status -o jsonExample Output#
Device Code (User) Authentication:
Handler Status Identity Tenant Expires
entra Authenticated user@example.com contoso.onmicrosoft.com 2026-05-04 15:30:00Service Principal Authentication:
Handler Status Identity IdentityType Tenant ClientId
entra Authenticated Service Principal (12345678...) service-principal tenant-id 12345678-1234-...GitHub Device Code Authentication:
Handler Status Identity Username Hostname Scopes
github Authenticated octocat octocat github.com gist, read:org, repo, workflowGitHub PAT Authentication:
Handler Status Identity Username IdentityType Scopes
github Authenticated octocat octocat service-principal gist, read:org, repo, workflowWhen not authenticated, the hint field tells you the exact command to run:
Handler Status Identity Tenant Expires Hint
entra Not Authenticated - - - run 'scafctl auth login entra' to authenticate
github Not Authenticated - - - run 'scafctl auth login github' to authenticate
gcp Not Authenticated - - - run 'scafctl auth login gcp' to authenticateScripting with –exit-code#
Use --exit-code to make the command exit non-zero when any handler is not authenticated – handy in CI pre-flight checks:
# Fail the script if not authenticated
scafctl auth status entra --exit-code || { echo "Not authenticated. Run: scafctl auth login entra"; exit 1; }# Fail the script if not authenticated
scafctl auth status entra --exit-code
if ($LASTEXITCODE -ne 0) { Write-Output "Not authenticated. Run: scafctl auth login entra"; exit 1; }Proactive Expiry Warnings (–warn-within)#
Use --warn-within <duration> to exit non-zero if any authenticated token will expire within the given window. This catches near-expiry tokens before they cause a mid-job failure:
# Exit non-zero if any token expires within 10 minutes
scafctl auth status --warn-within 10m
# Check a specific handler
scafctl auth status entra --warn-within 1h
# Combine with --exit-code for a full CI pre-flight check
scafctl auth status --exit-code --warn-within 15m || {
echo "Auth pre-flight failed -- not authenticated or token expiring soon"
exit 1
}# Exit non-zero if any token expires within 10 minutes
scafctl auth status --warn-within 10m
# Check a specific handler
scafctl auth status entra --warn-within 1h
# Combine with --exit-code for a full CI pre-flight check
scafctl auth status --exit-code --warn-within 15m
if ($LASTEXITCODE -ne 0) {
Write-Output "Auth pre-flight failed -- not authenticated or token expiring soon"
exit 1
}The JSON output includes a cachedTokens field showing how many access tokens are in the cache for each handler – useful for verifying token cache health:
scafctl auth status -o json | jq '.[].cachedTokens'(scafctl auth status -o json | ConvertFrom-Json).cachedTokensListing and Sorting Cached Tokens#
The auth list command shows metadata for all cached tokens (refresh tokens and access tokens) without revealing the actual token values:
# Show all cached tokens for all handlers
scafctl auth list
# Show cached tokens for a single handler
scafctl auth list entra
scafctl auth list github
scafctl auth list gcp
# Show only expired tokens
scafctl auth list --expired-only
# Show only valid tokens
scafctl auth list --valid-only
# Sort by expiry (soonest expiring first -- useful for spotting tokens about to expire)
scafctl auth list --sort expires-at
# Sort by handler name
scafctl auth list --sort handler
# Sort by scope
scafctl auth list --sort scope
# Output as JSON for scripting
scafctl auth list -o json
# Remove all expired access tokens from the cache (refresh tokens and valid tokens are preserved)
scafctl auth list --purge-expired
# Purge expired tokens for a specific handler
scafctl auth list entra --purge-expired# Show all cached tokens for all handlers
scafctl auth list
# Show cached tokens for a single handler
scafctl auth list entra
scafctl auth list github
scafctl auth list gcp
# Show only expired tokens
scafctl auth list --expired-only
# Show only valid tokens
scafctl auth list --valid-only
# Sort by expiry (soonest expiring first -- useful for spotting tokens about to expire)
scafctl auth list --sort expires-at
# Sort by handler name
scafctl auth list --sort handler
# Sort by scope
scafctl auth list --sort scope
# Output as JSON for scripting
scafctl auth list -o json
# Remove all expired access tokens from the cache (refresh tokens and valid tokens are preserved)
scafctl auth list --purge-expired
# Purge expired tokens for a specific handler
scafctl auth list entra --purge-expiredSort fields:
| Flag value | Sorted by |
|---|---|
handler | Auth handler name |
kind | Token kind (refresh / access) |
scope | OAuth scope string |
expires-at | Expiry time (soonest first) |
cached-at | When the token was cached (oldest first) |
The getTokenCommand column in the output shows the exact scafctl auth token command to retrieve each access token, making it easy to copy-paste for debugging.
Purging Expired Tokens (–purge-expired)#
Over time the token cache can accumulate expired access tokens. Use --purge-expired to clean them up. Refresh tokens and still-valid access tokens are not affected:
# Remove expired cache entries across all handlers
scafctl auth list --purge-expired
# Output:
# ✓ Purged 2 expired access token(s) from entra.
# ✓ Purged 0 expired access token(s) from github.
# Purge only for a specific handler
scafctl auth list gcp --purge-expired# Remove expired cache entries across all handlers
scafctl auth list --purge-expired
# Output:
# ✓ Purged 2 expired access token(s) from entra.
# ✓ Purged 0 expired access token(s) from github.
# Purge only for a specific handler
scafctl auth list gcp --purge-expired
--purge-expiredcannot be combined with--expired-onlyor--valid-only(it exits early without listing).
Using Auth in HTTP Providers#
The HTTP provider supports automatic authentication via the authProvider and scope properties.
Basic Example#
Create a file called graph-example.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: graph-example
version: 1.0.0
spec:
resolvers:
me:
type: object
resolve:
with:
- provider: http
inputs:
url: "https://graph.microsoft.com/v1.0/me"
method: GET
authProvider: entra
scope: "https://graph.microsoft.com/.default"Run it (requires prior authentication via scafctl auth login entra):
# Login with Microsoft Graph scope for consent
scafctl auth login entra --scope https://graph.microsoft.com/User.Read
# Then run the resolver
scafctl run resolver -f graph-example.yaml -o json# Login with Microsoft Graph scope for consent
scafctl auth login entra --scope https://graph.microsoft.com/User.Read
# Then run the resolver
scafctl run resolver -f graph-example.yaml -o jsonNote: If you see a “consent required” error, it means your login session doesn’t have consent for the requested API scope. Re-login with the
--scopeflag to grant consent:scafctl auth login entra --scope https://graph.microsoft.com/User.Read
When you run this, scafctl:
- Retrieves a cached token (or fetches a new one)
- Adds the
Authorization: Bearer <token>header - Executes the HTTP request
- Returns the response data
How It Works#
| Property | Description |
|---|---|
authProvider | The auth handler to use (e.g., entra) |
scope | The OAuth scope required for the API |
The token is validated to ensure it will remain valid for the duration of the request (timeout + 60 seconds buffer).
Automatic 401 Retry#
If the API returns HTTP 401 (Unauthorized), scafctl automatically:
- Requests a fresh token (bypassing the cache)
- Retries the request once with the new token
This handles cases where a cached token has been revoked.
Azure Resource Manager Example#
Add the following resolver to your solution’s spec.resolvers section:
spec:
resolvers:
subscription:
type: object
resolve:
with:
- provider: http
inputs:
url: "https://management.azure.com/subscriptions?api-version=2022-12-01"
method: GET
authProvider: entra
scope: "https://management.azure.com/.default"Key Vault Example#
Add the following resolver to your solution’s spec.resolvers section:
spec:
resolvers:
secret:
type: object
resolve:
with:
- provider: http
inputs:
url: "https://myvault.vault.azure.net/secrets/mysecret?api-version=7.4"
method: GET
authProvider: entra
scope: "https://vault.azure.net/.default"GitHub API Example#
Use the GitHub auth handler to authenticate API requests. Note that scope is not
needed for GitHub – scopes are fixed at login time:
spec:
resolvers:
repos:
type: object
resolve:
with:
- provider: http
inputs:
url: "https://api.github.com/user/repos?per_page=10&sort=updated"
method: GET
authProvider: githubRun it (requires prior authentication):
# Login with GitHub
scafctl auth login github
# Run the resolver
scafctl run resolver -f github-repos.yaml -o json# Login with GitHub
scafctl auth login github
# Run the resolver
scafctl run resolver -f github-repos.yaml -o jsonGitHub Enterprise Server API Example#
spec:
resolvers:
ghesRepos:
type: object
resolve:
with:
- provider: http
inputs:
url: "https://github.example.com/api/v3/user/repos"
method: GET
authProvider: githubGetting Tokens for Debugging#
The auth token command retrieves a valid access token for debugging. By
default it prints the raw token value to stdout so it composes cleanly with
shell scripts, curl, and tools like kubectl:
Breaking change:
auth tokennow prints the raw token by default. Earlier versions printed a masked table and required--rawfor the token value. The--rawflag is still accepted as an explicit alias. Use-o jsonor-o yamlto get the structured metadata object (handler, scope, expiry) instead of the bare token.
# Get a token for Microsoft Graph (Entra supports per-request scopes)
scafctl auth token entra --scope "https://graph.microsoft.com/.default"
# Get a GitHub token (uses scopes from login; --scope is not supported)
scafctl auth token github
# Get a GCP token
scafctl auth token gcp --scope "https://www.googleapis.com/auth/cloud-platform"# Get a token for Microsoft Graph (Entra supports per-request scopes)
scafctl auth token entra --scope "https://graph.microsoft.com/.default"
# Get a GitHub token (uses scopes from login; --scope is not supported)
scafctl auth token github
# Get a GCP token
scafctl auth token gcp --scope "https://www.googleapis.com/auth/cloud-platform"Note: The
--scopeflag is only supported onauth tokenfor handlers with thescopes-on-token-requestcapability (e.g., Entra ID and GCP). GitHub scopes are fixed at login time – usescafctl auth login github --scope <scope>to change them.
Example Output#
The default output is the raw token value, ready to pipe or assign:
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiI...Use JSON (or YAML) output to get the structured metadata object instead of the bare token:
scafctl auth token entra --scope "https://graph.microsoft.com/.default" -o jsonscafctl auth token entra --scope "https://graph.microsoft.com/.default" -o jsonToken Caching#
Access tokens are cached to disk (encrypted) and reused if they have sufficient remaining validity:
# Get a token valid for at least 5 minutes
scafctl auth token entra --scope "https://graph.microsoft.com/.default" --min-valid-for 5m# Get a token valid for at least 5 minutes
scafctl auth token entra --scope "https://graph.microsoft.com/.default" --min-valid-for 5mForce Refresh#
If you need a fresh token regardless of cache state (e.g., after permission changes), use --force-refresh:
# Force acquiring a new token, bypassing the cache
scafctl auth token entra --scope "https://graph.microsoft.com/.default" --force-refresh# Force acquiring a new token, bypassing the cache
scafctl auth token entra --scope "https://graph.microsoft.com/.default" --force-refreshPrinting the Raw Token (Scripting)#
Use --raw to print just the token value – ideal for shell scripting:
# Assign to a variable
export TOKEN=$(scafctl auth token gcp --scope "https://www.googleapis.com/auth/cloud-platform" --raw)
# Use directly with curl
curl -H "Authorization: Bearer $(scafctl auth token github --raw)" https://api.github.com/user# Assign to a variable
$env:TOKEN = $(scafctl auth token gcp --scope "https://www.googleapis.com/auth/cloud-platform" --raw)
# Use directly with curl
curl -H "Authorization: Bearer $(scafctl auth token github --raw)" https://api.github.com/userShell Export (eval-compatible)#
Use --export to get a shell export statement you can eval into the current shell. The variable is named <HANDLER>_TOKEN:
# Add a GCP token to the current shell environment
eval $(scafctl auth token gcp --scope "https://www.googleapis.com/auth/cloud-platform" --export)
echo $GCP_TOKEN
# Other handlers follow the same pattern
eval $(scafctl auth token github --export)
echo $GITHUB_TOKEN
eval $(scafctl auth token entra --scope "https://management.azure.com/.default" --export)
echo $ENTRA_TOKEN# Add a GCP token to the current shell environment
Invoke-Expression $(scafctl auth token gcp --scope "https://www.googleapis.com/auth/cloud-platform" --export)
Write-Output $GCP_TOKEN
# Other handlers follow the same pattern
Invoke-Expression $(scafctl auth token github --export)
Write-Output $GITHUB_TOKEN
Invoke-Expression $(scafctl auth token entra --scope "https://management.azure.com/.default" --export)
Write-Output $ENTRA_TOKENEmitting a Ready-to-Run curl Command#
Use --curl to print a curl command with the Authorization header already populated – great for quick API call reproduction without any jq piping:
# Emit a curl one-liner for Microsoft Graph
scafctl auth token entra --scope "https://graph.microsoft.com/.default" \
--curl --curl-url "https://graph.microsoft.com/v1.0/me"
# Output:
# curl -H "Authorization: Bearer eyJ..." "https://graph.microsoft.com/v1.0/me"
# Emit a curl one-liner for GCP
scafctl auth token gcp --scope "https://www.googleapis.com/auth/cloud-platform" \
--curl --curl-url "https://storage.googleapis.com/storage/v1/b?project=my-project"
# Emit without a URL (useful to inspect -- fills in a placeholder)
scafctl auth token github --curl
# Output:
# curl -H "Authorization: Bearer ghp_..." "<URL>"# Emit a curl one-liner for Microsoft Graph
scafctl auth token entra --scope "https://graph.microsoft.com/.default" `
--curl --curl-url "https://graph.microsoft.com/v1.0/me"
# Output:
# curl -H "Authorization: Bearer eyJ..." "https://graph.microsoft.com/v1.0/me"
# Emit a curl one-liner for GCP
scafctl auth token gcp --scope "https://www.googleapis.com/auth/cloud-platform" `
--curl --curl-url "https://storage.googleapis.com/storage/v1/b?project=my-project"
# Emit without a URL (useful to inspect -- fills in a placeholder)
scafctl auth token github --curl
# Output:
# curl -H "Authorization: Bearer ghp_..." "<URL>"Kubernetes ExecCredential (kubectl / oc)#
Use --exec-credential to emit a Kubernetes ExecCredential JSON object so
scafctl can act as a client-go credential plugin
for kubectl and
oc. This lets a cluster reuse the same scafctl-managed identity you already
logged in with – no separate kubeconfig token to rotate:
scafctl auth token entra --scope "<cluster-scope>/.default" --exec-credentialscafctl auth token entra --scope "<cluster-scope>/.default" --exec-credentialOutput (the status.token is consumed by kubectl; expirationTimestamp lets
kubectl cache the token until it expires):
{
"apiVersion": "client.authentication.k8s.io/v1",
"kind": "ExecCredential",
"status": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiI...",
"expirationTimestamp": "2026-02-04T16:30:00Z"
}
}When kubectl invokes the plugin it sets the KUBERNETES_EXEC_INFO
environment variable. scafctl auto-detects this: if KUBERNETES_EXEC_INFO
is present and you have not requested another render mode (--raw, --curl,
--export) or an explicit -o format, auth token emits an ExecCredential
automatically. scafctl also echoes back the apiVersion requested in
KUBERNETES_EXEC_INFO, so both client.authentication.k8s.io/v1 and
client.authentication.k8s.io/v1beta1 clients are supported.
Wire it into your kubeconfig under users[].user.exec:
users:
- name: my-cluster-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: scafctl
args:
- auth
- token
- entra
- --scope
- "<cluster-scope>/.default"
- --exec-credential
# Forwards KUBERNETES_EXEC_INFO so scafctl can echo the apiVersion.
provideClusterInfo: true
interactiveMode: IfAvailableFor a complete, copy-pasteable walkthrough see the kubectl exec credential example .
One-Step Cluster Login (kube login / kube logout)#
The --exec-credential wiring above is manual: you edit kubeconfig yourself. The
kube login and kube logout commands automate that setup. kube login runs
your auth handler, then writes the cluster, user (with the exec block), and
context entries for you. kube logout reverses it and clears the cached
credentials.
Two inputs play distinct roles. The handler is the credential source (which
identity provider authenticates you, for example entra). The cluster argument
(resolved through your cluster resolver) or --server selects the cluster
you connect to and supplies its API server and OIDC audience. The handler says
who you are, the cluster says where you connect.
When your cluster resolver records a default handler for a cluster, you can omit
--handler entirely and just name the cluster; pass --handler only to override
that default or when targeting a bare --server with no resolver.
Target a cluster known to your cluster resolver, or point at an API server directly:
# Resolver-backed: cluster name resolves to API server, audience, and handler
scafctl kube login prod
# Override the resolver's default handler
scafctl kube login prod --handler entra
# Explicit API server, no resolver required (handler must be named)
scafctl kube login --handler entra \
--server https://api.example.com:6443 \
--cluster-name prod --context prod --user prod --current
# Reuse the identity automatically on every kubectl/oc call
kubectl --context prod get pods# Resolver-backed: cluster name resolves to API server, audience, and handler
scafctl kube login prod
# Override the resolver's default handler
scafctl kube login prod --handler entra
# Explicit API server, no resolver required (handler must be named)
scafctl kube login --handler entra `
--server https://api.example.com:6443 `
--cluster-name prod --context prod --user prod --current
# Reuse the identity automatically on every kubectl/oc call
kubectl --context prod get podsCommon kube login flags:
| Flag | Purpose |
|---|---|
--handler | Auth handler to authenticate with. Optional when the resolver supplies a default for the cluster. |
--server | API server URL when no resolver is configured. |
--audience | OIDC audience the minted token must target. |
--profile | Auth profile baked into the exec args (for example work). |
--current | Set the new context as the current context. |
--verify | Confirm the authenticated identity via a post-login whoami (requires the kubeconfig provider). |
--kubeconfig | Target kubeconfig file (defaults to KUBECONFIG or ~/.kube/config). |
When the kubeconfig provider plugin is unavailable, kube login falls back to
writing a minimal static kubeconfig directly, so the workflow still works
offline.
kube logout removes the managed kubeconfig entry. When you pass --handler, it
revokes that handler’s cached credentials (unless --keep-credentials is set).
Without --handler it revokes the cluster’s resolver-supplied default handler on
a best-effort basis – if no default handler can be resolved, it removes only the
kubeconfig entry. Use --keep-credentials to skip credential revocation entirely:
# Remove the kubeconfig entry and revoke cached credentials
scafctl kube logout prod --handler entra
# Remove only the kubeconfig entry, keep credentials for other clusters
scafctl kube logout prod --keep-credentials# Remove the kubeconfig entry and revoke cached credentials
scafctl kube logout prod --handler entra
# Remove only the kubeconfig entry, keep credentials for other clusters
scafctl kube logout prod --keep-credentialsBoth commands support structured output (-o json, -o yaml) for automation.
For a complete walkthrough see the
Kubernetes login / logout example
.
OpenShift: One Command, OAuth or Entra (auto-routing)#
OpenShift clusters authenticate one of two ways, and kube login picks the
right path for you. It probes the API server’s
/.well-known/oauth-authorization-server endpoint to detect the cluster’s auth
type, then – when you supply neither --handler nor a resolver default –
routes automatically:
- OAuth cluster (OpenShift’s bundled OAuth server) -> the
openshifthandler runs a browser web-login (theoc login --webexperience) and holds the token. - OIDC / structured-auth cluster (delegated to Entra / Azure AD) -> the
existing
entrahandler mints a cluster-scoped token. The cluster’s audience comes from your resolver (oidcAudience), a static alias, or--audience– never a hardcoded tenant or client ID.
The same command works for either cluster; scafctl holds the credential and
serves it back to kubectl/oc on every call:
# One command regardless of auth type -- scafctl detects and routes
scafctl kube login mycluster --server https://api.mycluster.example.com:6443
# oauth cluster -> browser web login via the openshift handler
# oidc cluster -> entra handler, cluster-scoped token
# OIDC cluster reached directly needs an audience to scope the token
scafctl kube login --server https://api.mycluster.example.com:6443 \
--audience api://mycluster-client-id/.default
kubectl get pods # kubectl calls scafctl for a fresh token# One command regardless of auth type -- scafctl detects and routes
scafctl kube login mycluster --server https://api.mycluster.example.com:6443
# OIDC cluster reached directly needs an audience to scope the token
scafctl kube login --server https://api.mycluster.example.com:6443 `
--audience api://mycluster-client-id/.default
kubectl get pods # kubectl calls scafctl for a fresh tokenThe routing is a fallback only: an explicit --handler and a resolver-supplied
defaultHandler both win over the detected auth type, so a fleet inventory can
stamp defaultHandler / authType / audience on every entry and users just
run kube login <cluster>. When an OIDC cluster is auto-routed to entra but
no audience is available, login fails fast asking for --audience rather than
writing a kubeconfig entry that cannot authenticate.
Once logged in, the OpenShift integrated image registry is served through
scafctl’s credential helper, so docker/podman pulls fetch fresh tokens
without a separate docker login:
# Pull from the OpenShift integrated registry via the credential helper
podman pull default-route-openshift-image-registry.mycluster/myns/myimage# Pull from the OpenShift integrated registry via the credential helper
podman pull default-route-openshift-image-registry.mycluster/myns/myimagescafctl infers the openshift handler for the default registry route
(default-route-openshift-image-registry.*) and the in-cluster service host
(image-registry.openshift-image-registry.svc*). For a custom or renamed
registry route, add a customOAuth2 registry mapping (see
Custom OAuth2 registries
).
Decoding the JWT (Header + Payload)#
Use --decode to inspect the full JWT structure – both the header and the payload – without needing an external decoder tool. Signature validation is intentionally skipped; this is for debugging only:
# Decode and display the full JWT (header and payload)
scafctl auth token entra --scope "https://graph.microsoft.com/.default" --decode
# Output as JSON -- filter with jq
scafctl auth token entra --scope "https://graph.microsoft.com/.default" --decode -o json \
| jq '{alg: .header.alg, audience: .payload.aud, upn: .payload.upn, expires: .payload.exp_human}'scafctl auth token entra --scope "https://graph.microsoft.com/.default" --decode
# Output as JSON -- use ConvertFrom-Json to filter
$decoded = scafctl auth token entra --scope "https://graph.microsoft.com/.default" --decode -o json | ConvertFrom-Json
$decoded | Select-Object @{N='alg';E={$_.header.alg}}, @{N='audience';E={$_.payload.aud}}, @{N='upn';E={$_.payload.upn}}, @{N='expires';E={$_.payload.exp_human}}Example output (table):
Key Value
header.alg RS256
header.typ JWT
header.kid abc123...
payload.aud https://graph.microsoft.com
payload.iss https://login.microsoftonline.com/...
payload.sub A3ECB230-...
payload.oid 12345678-...
payload.upn user@example.com
payload.roles ["Directory.Read.All"]
payload.exp 1740000000
payload.exp_human 2026-02-19T22:13:20Z
payload.iat_human 2026-02-19T21:13:20ZThe header section tells you the signing algorithm (alg), key ID (kid), and token type (typ) – useful for confirming which key was used and troubleshooting signature or algorithm policy issues.
Unix timestamp fields (exp, iat, nbf, auth_time) are automatically augmented with a _human counterpart in RFC 3339 format.
This is the single fastest way to check:
- Which audience (
aud) the token is for - Which roles or scopes are included (
roles,scp) - Whether the token’s expiry (
exp_human) matches what you expect - Tenant, OID, and UPN for identity verification
Copying to Clipboard#
Use --clip to copy the token directly to your clipboard without it appearing in your terminal (useful when pasting into browser DevTools or Postman):
scafctl auth token entra --scope "https://management.azure.com/.default" --clip
# Output: ✓ Token copied to clipboard (expires in 58m42s).scafctl auth token entra --scope "https://management.azure.com/.default" --clip
# Output: ✓ Token copied to clipboard (expires in 58m42s).Inspecting Scoped Token Claims in Resolvers#
The identity provider’s scope input lets you mint a fresh access token for a
specific OAuth scope inside a resolver and inspect the claims or metadata parsed
from its JWT – without ever exposing the token value. This is useful for
preflight checks, per-API identity auditing, and debugging consent errors.
# Mint a token for an API scope and surface the caller's identity claims
resolve:
with:
- provider: identity
inputs:
operation: claims
scope: api://my-app/.default
# Check token expiry and flow for a management-plane scope
resolve:
with:
- provider: identity
inputs:
operation: status
scope: https://management.azure.com/.default
handler: entraKey differences from auth token:
auth token | identity provider (scoped) | |
|---|---|---|
| Returns the raw token | ✅ | ❌ (token is never exposed) |
| Parses JWT claims | ❌ | ✅ |
| Usable inside a resolver pipeline | ❌ | ✅ |
| Dry-run support | N/A | ✅ |
When the access token is opaque (not a decodable JWT – common with Microsoft
Graph tokens), claims will be null and a warning is emitted. Token metadata
such as expiry and type is still returned.
Scope restriction: The
scopeinput is only valid forclaimsandstatusoperations. Using it withgroupsorlistreturns an error.
Using the Token Directly#
Get the full token for use with other tools:
# Approach 1: --raw (simplest)
curl -H "Authorization: Bearer $(scafctl auth token entra --scope 'https://graph.microsoft.com/.default' --raw)" \
https://graph.microsoft.com/v1.0/me
# Approach 2: --curl (no jq required)
scafctl auth token entra --scope "https://graph.microsoft.com/.default" \
--curl --curl-url "https://graph.microsoft.com/v1.0/me" | bash
# Approach 3: JSON output + jq (most flexible)
TOKEN=$(scafctl auth token entra --scope "https://graph.microsoft.com/.default" -o json | jq -r '.accessToken')
curl -H "Authorization: Bearer $TOKEN" https://graph.microsoft.com/v1.0/me
# GitHub API example
curl -H "Authorization: Bearer $(scafctl auth token github --raw)" https://api.github.com/user/repos# Approach 1: --raw (simplest)
$token = scafctl auth token entra --scope 'https://graph.microsoft.com/.default' --raw
Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/me' -Headers @{ Authorization = "Bearer $token" }
# Approach 2: JSON output + ConvertFrom-Json
$token = (scafctl auth token entra --scope 'https://graph.microsoft.com/.default' -o json | ConvertFrom-Json).accessToken
Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/me' -Headers @{ Authorization = "Bearer $token" }Configuration#
You can configure authentication defaults in your config file (~/.config/scafctl/config.yaml or ~/.scafctl/config.yaml):
auth:
entra:
# Default tenant (use "organizations" for any work/school account)
tenantId: "08e70e8e-d05c-4449-a2c2-67bd0a9c4e79"
# Custom application ID (optional - uses scafctl's public client by default)
clientId: "your-app-client-id"
# Default scopes requested during login
defaultScopes:
- "openid"
- "profile"
- "offline_access"Configuration Reference#
| Field | Description | Default |
|---|---|---|
auth.entra.tenantId | Default Azure tenant ID | organizations |
auth.entra.clientId | Azure application (client) ID | scafctl public client |
auth.entra.defaultScopes | Scopes requested during login | openid, profile, offline_access |
Using a Custom Application#
If you need to use your own Azure application registration:
- Register an application in Azure Entra ID
- Configure it as a public client (mobile/desktop)
- Add the required API permissions
- Set
auth.entra.clientIdin your config
GCP Configuration#
You can configure a custom OAuth client for GCP to avoid depending on gcloud ADC:
auth:
gcp:
# Custom OAuth 2.0 client (bypasses gcloud ADC)
clientId: "123456789-abc123.apps.googleusercontent.com"
clientSecret: "GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxx"
# Default scopes requested during login
defaultScopes:
- "openid"
- "https://www.googleapis.com/auth/cloud-platform"
# Optional: impersonate a service account
impersonateServiceAccount: "deploy@my-project.iam.gserviceaccount.com"
# Optional: default project
project: "my-project-123"| Field | Description | Default |
|---|---|---|
auth.gcp.clientId | OAuth 2.0 client ID | (empty – uses gcloud ADC) |
auth.gcp.clientSecret | OAuth 2.0 client secret | (empty) |
auth.gcp.defaultScopes | Scopes requested during login | openid, cloud-platform |
auth.gcp.impersonateServiceAccount | Service account to impersonate | (empty) |
auth.gcp.project | Default GCP project ID | (empty) |
For a complete guide on creating the OAuth client with
gcloudcommands, see GCP Custom OAuth Client Setup .
Logging Out#
To clear stored credentials:
# Logout from Entra ID
scafctl auth logout entra
# Logout from GitHub
scafctl auth logout github
# Logout from GCP
scafctl auth logout gcp
# Logout from all registered handlers at once (prompts for confirmation)
scafctl auth logout --all
# Skip the confirmation prompt (for scripts and CI)
scafctl auth logout --all --yes
scafctl auth logout --all -y
# Force clear credentials even if not currently logged in
scafctl auth logout entra --force# Logout from Entra ID
scafctl auth logout entra
# Logout from GitHub
scafctl auth logout github
# Logout from GCP
scafctl auth logout gcp
# Logout from all registered handlers at once (prompts for confirmation)
scafctl auth logout --all
# Skip the confirmation prompt (for scripts and CI)
scafctl auth logout --all --yes
scafctl auth logout --all -y
# Force clear credentials even if not currently logged in
scafctl auth logout entra --forceThis removes:
- The stored refresh token (or access token for GitHub)
- All cached access tokens
- Token metadata
Dry Run#
Use --dry-run to see which credentials would be removed without actually removing them. Useful before running --all in a shared or production environment:
# Preview what would be removed for Entra
scafctl auth logout entra --dry-run
# Output: [dry-run] Would log out from Microsoft Entra ID (cached tokens and refresh token would be removed).
# Preview across all handlers
scafctl auth logout --all --dry-run# Preview what would be removed for Entra
scafctl auth logout entra --dry-run
# Output: [dry-run] Would log out from Microsoft Entra ID (cached tokens and refresh token would be removed).
# Preview across all handlers
scafctl auth logout --all --dry-runExample Output#
✓ Successfully logged out from Microsoft Entra ID.Auth Diagnostics#
The auth diagnose command (alias: auth doctor) runs a series of health checks
and reports any issues with your auth configuration. It’s the first command to run
when troubleshooting auth problems:
# Run all checks and print a human-readable report
scafctl auth diagnose
# Alias
scafctl auth doctor
# Scope checks to a single handler (skips checks for the others)
scafctl auth diagnose entra
scafctl auth diagnose github
scafctl auth diagnose gcp
# Also attempt a live token fetch for each authenticated handler
scafctl auth diagnose --live-token
# Scope live-token check to one handler
scafctl auth diagnose entra --live-token
# Output as JSON for automated pipelines
scafctl auth diagnose -o json# Run all checks and print a human-readable report
scafctl auth diagnose
# Alias
scafctl auth doctor
# Scope checks to a single handler (skips checks for the others)
scafctl auth diagnose entra
scafctl auth diagnose github
scafctl auth diagnose gcp
# Also attempt a live token fetch for each authenticated handler
scafctl auth diagnose --live-token
# Scope live-token check to one handler
scafctl auth diagnose entra --live-token
# Output as JSON for automated pipelines
scafctl auth diagnose -o jsonWhat It Checks#
| Category | What is checked |
|---|---|
registry | Auth handlers are registered and available |
config | Config file presence; auth.entra, auth.github, auth.gcp sections |
env | Relevant environment variables (AZURE_*, GITHUB_TOKEN, GOOGLE_*) |
clock-skew | System clock is validated against an external time source; warns if skew exceeds 5 minutes (clock skew causes token validation failures) |
handler | Each handler’s authentication status; hints for unauthenticated handlers |
cache | Token cache health – count and number of expired cached tokens |
live | (Only with --live-token) Performs an actual GetToken call to confirm end-to-end flow |
Example Output#
✅ [ok] auth registry: registered handlers: [entra gcp github]
⚠️ [warn] config file: config file not found -- using built-in defaults
✅ [ok] env GITHUB_TOKEN: GitHub personal access token -- set
✅ [ok] env gcp: gcloud ADC: gcloud Application Default Credentials file found
✅ [ok] entra: authenticated: authenticated as "user@example.com", expires in 58m
⚠️ [warn] entra: token cache: 3 cached token(s), 1 expired
✅ [ok] gcp: authenticated: authenticated as "gcloud ADC (application default credentials)"
✅ [ok] gcp: token cache: 1 cached token(s)
⚠️ [warn] github: authenticated: not authenticated -- run 'scafctl auth login github'
⚠️ Diagnostics complete: 3 warning(s), 5 ok (no failures)Exit Codes#
| Condition | Exit code |
|---|---|
| All checks pass or warn | 0 |
| Any check fails | non-zero |
Warnings (unauthenticated handler, expired tokens, missing config file) do not produce a non-zero exit code on their own. Only hard failures (registry empty, handler init error) do.
Using in CI Preflight#
# Verify end-to-end auth before running a pipeline
scafctl auth diagnose --live-token
if [ $? -ne 0 ]; then
echo "Auth health check failed. Check 'scafctl auth diagnose' output."
exit 1
fi# Verify end-to-end auth before running a pipeline
scafctl auth diagnose --live-token
if ($? -ne 0) {
Write-Output "Auth health check failed. Check 'scafctl auth diagnose' output."
exit 1
}Troubleshooting#
“Not authenticated” Error#
If you see this error when running a solution:
not authenticated: please run 'scafctl auth login entra'Solution: Run scafctl auth login entra to authenticate.
For GitHub:
not authenticated: please run 'scafctl auth login github'Solution: Run scafctl auth login github or set GITHUB_TOKEN environment variable.
Token Expired#
If you see:
credentials expired: please run 'scafctl auth login entra'Your refresh token has expired (typically after 90 days of inactivity). Log in again:
scafctl auth login entrascafctl auth login entraConsent Required#
If you see:
consent required: please login with the required scopeYour login session does not have consent for the API scope your resolver is requesting. Re-login with the --scope flag to grant consent:
# For Microsoft Graph APIs
scafctl auth login entra --scope https://graph.microsoft.com/User.Read
# For Azure Resource Manager APIs
scafctl auth login entra --scope https://management.azure.com/user_impersonation# For Microsoft Graph APIs
scafctl auth login entra --scope https://graph.microsoft.com/User.Read
# For Azure Resource Manager APIs
scafctl auth login entra --scope https://management.azure.com/user_impersonationThe --scope flag tells Azure to request user consent for that specific API during the login flow.
AADSTS500113: No Reply Address Registered#
If the browser shows this error during interactive login:
AADSTS500113: No reply address is registered for the application.This means the app registration does not have a redirect URI matching http://localhost. The default Azure CLI client ID already has this registered; this error only occurs with custom --client-id values.
Fix options:
Register the redirect URI (recommended): In the Azure portal, go to App registrations → your app → Authentication → Add a platform → Mobile and desktop applications, then add
http://localhost.Use a specific port: If the app registration only allows a specific URI like
http://localhost:8400:
scafctl auth login entra --client-id YOUR_CLIENT_ID --callback-port 8400 scafctl auth login entra --client-id YOUR_CLIENT_ID --callback-port 8400- Use device code flow: Device code does not require a redirect URI:
scafctl auth login entra --client-id YOUR_CLIENT_ID --flow device-code scafctl auth login entra --client-id YOUR_CLIENT_ID --flow device-codeLogin Times Out With No Error#
If scafctl auth login entra times out after 5 minutes with no error in the
terminal, the most common cause is the AADSTS500113 error above – check the
browser tab for an error message. The improved timeout message will now suggest
checking redirect URI registration.
Wrong Tenant#
If you’re getting 401 errors but you’re authenticated, you may be authenticated to the wrong tenant:
# Check current auth status
scafctl auth status entra
# Log out and log in to the correct tenant
scafctl auth logout entra
scafctl auth login entra --tenant correct-tenant-id# Check current auth status
scafctl auth status entra
# Log out and log in to the correct tenant
scafctl auth logout entra
scafctl auth login entra --tenant correct-tenant-idScope Issues#
If you’re getting 403 (Forbidden) errors, the token may not have the required permissions:
- Ensure you’re using the correct scope (e.g.,
https://graph.microsoft.com/.default) - Verify your Azure application has the required API permissions
- For some APIs, admin consent may be required
Checking Token Claims#
Use --decode on auth token to inspect JWT claims directly without needing an
external tool or manual base64 decoding:
# Decode and display claims in table format (no signature validation)
scafctl auth token entra --scope "https://graph.microsoft.com/.default" --decode
# Output as JSON for further processing (e.g., with jq)
scafctl auth token entra --scope "https://graph.microsoft.com/.default" --decode -o json | jq '.aud,.upn,.roles'scafctl auth token entra --scope "https://graph.microsoft.com/.default" --decode
# Output as JSON for further processing
$decoded = scafctl auth token entra --scope "https://graph.microsoft.com/.default" --decode -o json | ConvertFrom-Json
$decoded.aud, $decoded.upn, $decoded.rolesUnix timestamp fields (exp, iat, nbf, auth_time) are automatically augmented
with a _human RFC 3339 counterpart so you can read them without converting.
Useful things to verify:
aud– correct audience for the API you’re callingscp/roles– scopes or app roles grantedexp_human– actual token expiry in human-readable formupn/unique_name/preferred_username– the authenticated identity
Debug Logging#
Enable debug logging to see detailed auth information:
scafctl --log-level -1 auth status entra
scafctl --log-level -1 run solution -f mysolution.yamlscafctl --log-level -1 auth status entra
scafctl --log-level -1 run solution -f mysolution.yamlSecret Store Issues#
scafctl uses your system’s secret store (Keychain on macOS, Windows Credential Manager, or Secret Service on Linux). If you’re having issues:
- macOS: Check Keychain Access for
scafctl.auth.entra.*orscafctl.auth.github.*entries - Windows: Check Credential Manager for
scafctl.auth.entra.*orscafctl.auth.github.*entries - Linux: Ensure
gnome-keyringorkwalletis running
GitHub: “Bad credentials” Error#
If you see a 401 error with “Bad credentials” when using GitHub auth:
- Your PAT may have expired or been revoked
- Generate a new PAT at https://github.com/settings/tokens
- Set the new token:
export GITHUB_TOKEN="ghp_..." - Re-login:
scafctl auth login github
GitHub: Insufficient Scopes#
If you get 403 (Forbidden) errors on GitHub API calls:
- Check what scopes your token has:
scafctl auth status github - GitHub scopes are fixed at login time and cannot be changed per-request. Login again with the required scopes:
scafctl auth logout github scafctl auth login github --scope repo --scope read:orgscafctl auth logout github scafctl auth login github --scope repo --scope read:org - For PATs, ensure the token was created with sufficient scopes
GitHub Enterprise Server: Connection Issues#
If you can’t connect to your GHES instance:
- Verify the hostname is correct:
scafctl auth status github - Ensure the GHES instance has the device flow enabled
- Check network connectivity to the GHES instance
- Try with explicit hostname:
scafctl auth login github --hostname github.example.com
Custom OAuth2 Handlers#
You can add OAuth2 handlers for any service by configuring them in your
~/.config/scafctl/config.yaml:
auth:
customOAuth2:
- name: quay
displayName: "Quay.io"
authorizeURL: "https://quay.io/oauth/authorize"
tokenURL: "https://quay.io/oauth/access_token"
clientID: "your-app-client-id"
defaultFlow: interactive
responseType: token # Quay only supports implicit grant (response_type=token)
scopes:
- "repo:read"
registry: "quay.io"
registryUsername: "$oauthtoken"Once configured, custom handlers work exactly like built-in handlers:
# Login
scafctl auth login quay
# Check status
scafctl auth status quay
# Logout
scafctl auth logout quay
# Auto-detected for catalog login (via the 'registry' field)
scafctl catalog login quay.ioCustom handlers support all three OAuth2 flows:
- Interactive (authorization code + PKCE) – requires
authorizeURL. SetresponseType: tokenfor implicit grant servers. SetdisablePKCE: truefor servers that support auth code but reject PKCE parameters - Device code (RFC 8628) – requires
deviceAuthURL - Client credentials – requires
clientSecret
For advanced configurations including token exchange and identity verification, see examples/auth/custom-oauth2-config.md .
Pinning an External Plugin Handler (auth.handlers.<name>.plugin)#
The customOAuth2 block above adds a handler using scafctl’s built-in generic
OAuth2 engine – no plugin binary required. When you instead need a plugin auth
handler (a standalone gRPC binary – e.g. written in another language, or with
logic the generic engine can’t express), pin it to a name under
auth.handlers.<name>.plugin. scafctl auto-fetches the plugin from a configured
catalog on first use. Neither approach requires recompiling scafctl:
catalogs:
- name: acme
type: oci
url: oci://ghcr.io/acme/scafctl-handlers
auth:
handlers:
acme:
plugin:
ref: acme-auth # catalog artifact name (defaults to the handler name)
version: ">=1.0.0" # semver constraint or "latest" (the default)
# Third-party handlers do not inherit the official trusted domains, so
# declare any device-code verification domains explicitly:
trustedVerificationDomains:
- login.acme.example.com
# Any other keys are forwarded opaquely to the plugin as its settings:
tenantId: "..."Then it behaves like any other handler:
scafctl auth login acme
scafctl auth status acmeNotes:
- Precedence. A config pin wins over the built-in official manifest, so
auth.handlers.github.pluginrepoints thegithubname at your own artifact. (Unlike acustomOAuth2entry – which may not reuse a reserved official name such asgithub,gcp,entra, oropenshift– a deliberate plugin pin is allowed to override one.) - Bare names work too. If the artifact name matches the handler name, you can
skip the pin and just reference the name; scafctl auto-fetches it from your
configured catalogs. The pin is only needed to fix a
ref/versionor to alias a differently named artifact. - Policy switches.
settings.disableThirdPartyAuthHandlersblocks catalog and config-pin resolution;settings.disableOfficialAuthHandlersblocks the built-in official manifest.
Handler Lifecycle Management#
Official auth handlers (github, gcp, entra, openshift) are delivered as plugin
binaries and downloaded automatically on first use (e.g., during scafctl auth login github). You can also manage them explicitly.
Listing Available Handlers#
# Show all registered handlers with flows and capabilities
scafctl auth handlers
scafctl auth handlers -o jsonPre-installing a Handler#
Download a handler plugin before first use (e.g., for air-gapped environments or to inspect capabilities):
# Install a specific handler
scafctl auth handlers install github
scafctl auth handlers install entra
# Force re-download (replaces cached binary)
scafctl auth handlers install entra --forceRemoving a Handler#
Remove cached handler plugin binaries to free disk space or force a fresh download on next use:
# Remove a handler's cached binary
scafctl auth handlers remove github
scafctl auth handlers remove entraAfter removal, the handler will be re-downloaded automatically on next
scafctl auth login <handler>.
Lazy Loading Behavior#
Auth handlers are loaded lazily – plugin subprocesses are started only when an operation requires I/O (login, token refresh, listing cached tokens). This means:
scafctl auth handlerslists registered handler names without network I/Oscafctl auth listqueries all eagerly-registered handlers (initializing them as needed) but does not trigger auto-download of unconfigured handlers- Running
scafctl auth login <handler>triggers auto-download if the handler is not yet cached locally
This design avoids surprise network I/O and plugin downloads until explicitly requested.
Shell Completion#
The login and logout commands support shell completion for handler names.
If you have shell completions enabled for scafctl, pressing <Tab> after
scafctl auth login or scafctl auth logout will suggest available handler
names (e.g., entra, github, gcp, and any custom OAuth2 handlers).
Next Steps#
- CEL Expressions Tutorial – Master CEL expressions and extension functions
- Go Templates Tutorial – Generate files with Go template rendering
- Resolver Tutorial – More HTTP examples in resolver pipelines
- Provider Reference – Complete provider documentation