oidc

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

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:

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

Examples

Constants

This section is empty.

Variables

View Source
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

func ClientCredentials(ctx context.Context, opts ...LoginOption) (*oauth2.Token, error)

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

func DeviceLogin(ctx context.Context, opts ...LoginOption) (*oauth2.Token, error)

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

func Login(ctx context.Context, gatewayName string, opts ...LoginOption) (*oauth2.Token, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL