Documentation
¶
Overview ¶
Package oidc provides OIDC authentication flows for the OpenShell SDK.
The package supports four authentication flows:
- Authorization Code with PKCE (interactive browser-based login)
- Keyboard flow (manual URL copy and code paste for headless environments)
- Device Code flow (RFC 8628, for input-constrained devices)
- Client Credentials grant (non-interactive service account authentication)
Gateway-Aware Login ¶
The primary use case is gateway-aware login, where OIDC provider configuration is read from a gateway's metadata.json file:
token, err := oidc.Login(ctx, "my-gateway")
if err != nil {
log.Fatal(err)
}
After successful authentication, tokens are persisted to disk in the gateway directory as oidc_token.json, compatible with gateway.NewClient and the existing [gateway.diskTokenSource].
Standalone Login ¶
For OIDC providers not tied to an OpenShell gateway, use explicit configuration:
token, err := oidc.Login(ctx, "",
oidc.WithIssuer("https://auth.example.com"),
oidc.WithClientID("my-app"),
oidc.WithInMemory(),
)
Device Code Flow ¶
For environments without a browser:
token, err := oidc.DeviceLogin(ctx,
oidc.WithIssuer("https://auth.example.com"),
oidc.WithClientID("my-app"),
)
Client Credentials ¶
For non-interactive service accounts:
token, err := oidc.ClientCredentials(ctx,
oidc.WithIssuer("https://auth.example.com"),
oidc.WithClientID("my-service"),
oidc.WithClientSecret("secret"),
)
Error Handling ¶
The package provides typed sentinel errors for precise failure classification:
- ErrDiscovery: OIDC discovery fetch or parse failed
- ErrAuthCode: Authorization code exchange failed
- ErrDeviceCode: Device code flow failed
- ErrClientCredentials: Client credentials exchange failed
- ErrTimeout: Interactive flow timed out
- ErrCallbackServer: Localhost callback server failed to start
- ErrTokenPersist: Token disk write failed
- ErrOIDCConfig: Gateway metadata missing OIDC fields
All errors support errors.Is for classification.
Thread Safety ¶
All exported functions are safe for concurrent use from multiple goroutines. OIDC discovery documents are cached in memory per issuer URL for the lifetime of the process.
Index ¶
- Variables
- func ClientCredentials(ctx context.Context, opts ...LoginOption) (*oauth2.Token, error)
- func DeviceLogin(ctx context.Context, opts ...LoginOption) (*oauth2.Token, error)
- func Login(ctx context.Context, gatewayName string, opts ...LoginOption) (*oauth2.Token, error)
- type LoginOption
- func WithCallbackPort(port int) LoginOption
- func WithClientID(id string) LoginOption
- func WithClientSecret(secret string) LoginOption
- func WithDisplayFunc(fn func(verificationURL, userCode string)) LoginOption
- func WithGateway(name string) LoginOption
- func WithInMemory() LoginOption
- func WithIssuer(url string) LoginOption
- func WithKeyboardFlow() LoginOption
- func WithScopes(scopes ...string) LoginOption
- func WithTimeout(d time.Duration) LoginOption
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrDiscovery is returned when the OIDC discovery document // (.well-known/openid-configuration) cannot be fetched or parsed. ErrDiscovery = errors.New("oidc: discovery failed") // ErrAuthCode is returned when the authorization code exchange // fails (invalid code, expired code, provider error). ErrAuthCode = errors.New("oidc: auth code exchange failed") // ErrDeviceCode is returned when the device code flow fails // (request error, expired device code, provider error). ErrDeviceCode = errors.New("oidc: device code flow failed") // ErrClientCredentials is returned when the client credentials // grant fails (invalid credentials, provider error). The error // message never contains the client secret. ErrClientCredentials = errors.New("oidc: client credentials exchange failed") // ErrTimeout is returned when an interactive login flow // (browser, keyboard, or device code) exceeds its deadline. ErrTimeout = errors.New("oidc: login timed out") // ErrCallbackServer is returned when the localhost HTTP server // for the authorization code redirect cannot bind to any port. ErrCallbackServer = errors.New("oidc: callback server failed") // ErrTokenPersist is returned when the token cannot be written // to disk (permission error, invalid path). ErrTokenPersist = errors.New("oidc: token persistence failed") // ErrOIDCConfig is returned when gateway metadata is missing // the required oidc_issuer or oidc_client_id fields. ErrOIDCConfig = errors.New("oidc: gateway OIDC config missing") )
Sentinel errors for OIDC authentication failures. All wrapped errors returned by this package support classification via errors.Is.
Functions ¶
func ClientCredentials ¶
ClientCredentials performs a non-interactive OAuth2 client credentials grant (RFC 6749 Section 4.4). It requires WithIssuer, WithClientID, and WithClientSecret (or WithGateway combined with WithClientSecret).
This flow is intended for service accounts and machine-to-machine authentication. No user interaction occurs. The returned token typically contains only an access token (no refresh token).
The client secret is never included in error messages (FR-014).
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rhuss/openshell-sdk-go/openshell/v1/oidc"
)
func main() {
if false {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
token, err := oidc.ClientCredentials(ctx,
oidc.WithIssuer("https://auth.example.com"),
oidc.WithClientID("my-service"),
oidc.WithClientSecret("service-secret"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Service authenticated. Token type: %s\n", token.TokenType)
}
fmt.Println("ok")
}
Output: ok
Example (Gateway) ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rhuss/openshell-sdk-go/openshell/v1/oidc"
)
func main() {
if false {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
token, err := oidc.ClientCredentials(ctx,
oidc.WithGateway("my-gateway"),
oidc.WithClientSecret("service-secret"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Service authenticated via gateway. Token type: %s\n", token.TokenType)
}
fmt.Println("ok")
}
Output: ok
func DeviceLogin ¶
DeviceLogin performs an OAuth2 device authorization grant (RFC 8628).
The flow requests a device code and user code from the provider's device authorization endpoint, displays them to the user (via WithDisplayFunc or stdout), and polls the token endpoint until the user completes authorization.
Required options: WithIssuer and WithClientID, or WithGateway.
The polling loop respects the provider's interval and handles the following token endpoint error codes:
- "authorization_pending": continue polling at the current interval
- "slow_down": increase the polling interval by 5 seconds (RFC 8628 Section 3.5)
- "expired_token": the device code has expired, return ErrDeviceCode
- any other error: return ErrDeviceCode
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rhuss/openshell-sdk-go/openshell/v1/oidc"
)
func main() {
if false {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
token, err := oidc.DeviceLogin(ctx,
oidc.WithIssuer("https://auth.example.com"),
oidc.WithClientID("my-device-app"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Device authorized. Token expires at %s\n", token.Expiry.Format(time.RFC3339))
}
fmt.Println("ok")
}
Output: ok
Example (CustomDisplay) ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rhuss/openshell-sdk-go/openshell/v1/oidc"
)
func main() {
if false {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
token, err := oidc.DeviceLogin(ctx,
oidc.WithIssuer("https://auth.example.com"),
oidc.WithClientID("my-tui-app"),
oidc.WithDisplayFunc(func(verificationURL, userCode string) {
fmt.Printf("Please visit: %s\n", verificationURL)
fmt.Printf("Enter code: %s\n", userCode)
}),
)
if err != nil {
log.Fatal(err)
}
_ = token
}
fmt.Println("ok")
}
Output: ok
func Login ¶
Login performs an interactive OIDC authorization code login.
When gatewayName is non-empty, Login resolves OIDC configuration (issuer URL and client ID) from the gateway's metadata.json file and persists tokens to the gateway directory.
When gatewayName is empty, the caller must provide WithIssuer and WithClientID options explicitly.
Before starting an interactive flow, Login checks for an existing valid token on disk (FR-019). If a valid, non-expired token is found, it is returned immediately without user interaction.
The flow attempts to open a browser for authorization. If the browser cannot be opened, or if WithKeyboardFlow is set, the keyboard fallback flow is used instead.
Example (Gateway) ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rhuss/openshell-sdk-go/openshell/v1/oidc"
)
func main() {
if false {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
token, err := oidc.Login(ctx, "my-gateway")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Authenticated. Token expires at %s\n", token.Expiry.Format(time.RFC3339))
}
fmt.Println("ok")
}
Output: ok
Example (Keyboard) ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rhuss/openshell-sdk-go/openshell/v1/oidc"
)
func main() {
if false {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
token, err := oidc.Login(ctx, "my-gateway",
oidc.WithKeyboardFlow(),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Authenticated via keyboard flow. Token type: %s\n", token.TokenType)
}
fmt.Println("ok")
}
Output: ok
Example (Standalone) ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/rhuss/openshell-sdk-go/openshell/v1/oidc"
)
func main() {
if false {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
token, err := oidc.Login(ctx, "",
oidc.WithIssuer("https://auth.example.com"),
oidc.WithClientID("my-app"),
oidc.WithInMemory(),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Access token: %s...\n", token.AccessToken[:10])
}
fmt.Println("ok")
}
Output: ok
Types ¶
type LoginOption ¶
type LoginOption func(*loginConfig)
LoginOption configures a login attempt. Use the With* functions to create option values.
func WithCallbackPort ¶
func WithCallbackPort(port int) LoginOption
WithCallbackPort sets a fixed port for the localhost callback server. By default the server tries port 8000, then 18000.
func WithClientID ¶
func WithClientID(id string) LoginOption
WithClientID sets the OAuth2 client ID. Required for standalone flows (when no gateway name is provided to Login).
func WithClientSecret ¶
func WithClientSecret(secret string) LoginOption
WithClientSecret sets the client secret for the client credentials grant. Required for ClientCredentials.
func WithDisplayFunc ¶
func WithDisplayFunc(fn func(verificationURL, userCode string)) LoginOption
WithDisplayFunc sets a custom display function for the device code flow. The function receives the verification URL and user code that the user must enter to authorize the device. If not set, the default behavior prints to stdout.
func WithGateway ¶
func WithGateway(name string) LoginOption
WithGateway sets the gateway name for DeviceLogin and ClientCredentials. When set, OIDC config is read from the gateway's metadata.json and tokens are persisted to the gateway directory.
func WithInMemory ¶
func WithInMemory() LoginOption
WithInMemory skips persisting the token to disk. The returned token is only available in memory for the lifetime of the process.
func WithIssuer ¶
func WithIssuer(url string) LoginOption
WithIssuer sets the OIDC issuer URL. Required for standalone flows (when no gateway name is provided to Login).
func WithKeyboardFlow ¶
func WithKeyboardFlow() LoginOption
WithKeyboardFlow forces the keyboard flow (manual URL copy and code paste) instead of attempting to open a browser.
func WithScopes ¶
func WithScopes(scopes ...string) LoginOption
WithScopes overrides the default scopes (openid, profile, email). The provided scopes replace the defaults entirely.
func WithTimeout ¶
func WithTimeout(d time.Duration) LoginOption
WithTimeout sets the maximum duration for interactive login flows. The default is 2 minutes.