Authsome

OAuth2 Provider

Run Authsome as an OAuth2 authorization server with Authorization Code, PKCE, and Client Credentials flows.

Authsome can act as a full OAuth2 authorization server, allowing third-party applications to authenticate users and obtain access tokens through standard OAuth2 flows. This is distinct from Social Login (where Authsome is an OAuth2 client) — here, Authsome issues its own tokens on behalf of your users.

Setup

import (
    "github.com/xraph/authsome"
    "github.com/xraph/authsome/plugins/oauth2provider"
)

eng, err := authsome.New(
    authsome.WithStore(store),
    authsome.WithPlugin(password.New()),
    authsome.WithPlugin(oauth2provider.New()),
    authsome.WithConfig(authsome.Config{
        AppID: "myapp",
        OAuth2: authsome.OAuth2Config{
            Enabled:                true,
            Issuer:                 "https://auth.myapp.com",
            AuthorizationEndpoint:  "/oauth2/authorize",
            TokenEndpoint:          "/oauth2/token",
            IntrospectionEndpoint:  "/oauth2/introspect",
            RevocationEndpoint:     "/oauth2/revoke",
            JWKSEndpoint:           "/.well-known/jwks.json",
            AccessTokenTTL:         "1h",
            RefreshTokenTTL:        "30d",
            AuthorizationCodeTTL:   "10m",
            RequirePKCE:            true,
            AllowedScopes:          []string{"openid", "profile", "email", "offline_access"},
        },
    }),
)

Client registration

OAuth2 clients must be registered before they can request tokens. Clients represent third-party applications or services that consume your authentication API.

Create a client

POST /v1/auth/oauth2/clients

{
  "name": "My Mobile App",
  "type": "public",
  "redirect_uris": ["myapp://callback", "https://myapp.com/callback"],
  "scopes": ["openid", "profile", "email"],
  "grant_types": ["authorization_code"],
  "app_id": "myapp"
}

Response:

{
  "client_id": "aoc2_01jb...",
  "client_secret": "sec_a3f8c9d4e5f2g7h1...",
  "name": "My Mobile App",
  "type": "public",
  "redirect_uris": ["myapp://callback", "https://myapp.com/callback"],
  "scopes": ["openid", "profile", "email"],
  "grant_types": ["authorization_code"],
  "created_at": "2024-11-01T10:00:00Z"
}

Important: The client_secret is only returned once at creation time. Store it securely. Public clients (mobile/SPA apps) should use PKCE instead of a client secret.

Client types

TypeDescriptionUse case
confidentialCan securely store a client secretServer-side applications, backend services
publicCannot securely store a client secretMobile apps, single-page applications, CLI tools

In Go

client := &oauth2provider.Client{
    Name:         "My Mobile App",
    Type:         "public",
    RedirectURIs: []string{"myapp://callback"},
    Scopes:       []string{"openid", "profile", "email"},
    GrantTypes:   []string{"authorization_code"},
}

err := engine.CreateOAuth2Client(ctx, appID, client)
// client.ID and client.Secret are now populated

Authorization Code flow with PKCE

The Authorization Code flow with PKCE (Proof Key for Code Exchange) is the recommended flow for all client types. It is required for public clients and strongly recommended for confidential clients.

Generate PKCE challenge: The client generates a random code_verifier and derives a code_challenge from it.

// Client-side
import "crypto/sha256"
import "encoding/base64"

codeVerifier := generateRandomString(64)
hash := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])

Authorization request: Redirect the user to the authorization endpoint.

GET /v1/auth/oauth2/authorize?
  response_type=code&
  client_id=aoc2_01jb...&
  redirect_uri=https://myapp.com/callback&
  scope=openid+profile+email&
  state=random_state_value&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256

If the user is not already signed in, they are redirected to the Authsome login page. After authentication (and MFA if enabled), the user is shown a consent screen listing the requested scopes. On approval, the user is redirected back to the client with an authorization code.

HTTP/1.1 302 Found
Location: https://myapp.com/callback?code=auth_01jb...&state=random_state_value

Token exchange: The client exchanges the authorization code for tokens.

POST /v1/auth/oauth2/token

Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=auth_01jb...&
redirect_uri=https://myapp.com/callback&
client_id=aoc2_01jb...&
code_verifier=the_original_code_verifier_string

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_d72b1ef8...",
  "scope": "openid profile email",
  "id_token": "eyJhbGciOiJSUzI1NiIs..."
}

Client Credentials grant

The Client Credentials grant is used for machine-to-machine authentication where no user context is needed. Only confidential clients can use this grant type.

POST /v1/auth/oauth2/token

Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&
client_id=aoc2_01jb...&
client_secret=sec_a3f8c9d4e5f2g7h1...&
scope=api:read api:write

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "api:read api:write"
}

No refresh token is issued for client credentials grants — the client simply requests a new token when the current one expires.

Token introspection

Resource servers can verify tokens by calling the introspection endpoint. This is useful for opaque tokens or when the resource server does not have access to the signing keys.

POST /v1/auth/oauth2/introspect

Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

token=eyJhbGciOiJSUzI1NiIs...

Response (active token):

{
  "active": true,
  "sub": "ausr_01j9...",
  "client_id": "aoc2_01jb...",
  "scope": "openid profile email",
  "iat": 1730451600,
  "exp": 1730455200,
  "iss": "https://auth.myapp.com",
  "token_type": "Bearer"
}

Response (expired or revoked token):

{
  "active": false
}

Token revocation

Clients can revoke tokens when they are no longer needed (e.g., on user logout):

POST /v1/auth/oauth2/revoke

Content-Type: application/x-www-form-urlencoded

token=eyJhbGciOiJSUzI1NiIs...&
token_type_hint=access_token&
client_id=aoc2_01jb...&
client_secret=sec_a3f8c9d4e5f2g7h1...

The endpoint always returns 200 OK regardless of whether the token was found, to prevent token enumeration.

Built-in scopes

ScopeDescription
openidRequired for OIDC. Returns an ID token with the sub claim
profileAccess to user's name, username, and profile image
emailAccess to user's email address and email verification status
phoneAccess to user's phone number and phone verification status
offline_accessRequest a refresh token for long-lived access

Custom scopes

Register custom scopes in the configuration:

OAuth2: authsome.OAuth2Config{
    AllowedScopes: []string{
        "openid", "profile", "email",
        "api:read", "api:write", "admin",
    },
},

When a third-party client requests authorization, the user is shown a consent screen listing the requested scopes. The consent decision is remembered per user-client pair. To force re-consent:

GET /v1/auth/oauth2/authorize?...&prompt=consent

JWKS endpoint

The JSON Web Key Set (JWKS) endpoint publishes the public keys used to verify JWT access tokens and ID tokens:

GET /.well-known/jwks.json

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-2024-01",
      "use": "sig",
      "alg": "RS256",
      "n": "0vx7agoebGc...",
      "e": "AQAB"
    }
  ]
}

Resource servers should cache the JWKS and refresh it periodically or when they encounter a token signed with an unknown kid.

OpenID Connect discovery

Authsome publishes an OpenID Connect discovery document:

GET /.well-known/openid-configuration

{
  "issuer": "https://auth.myapp.com",
  "authorization_endpoint": "https://auth.myapp.com/v1/auth/oauth2/authorize",
  "token_endpoint": "https://auth.myapp.com/v1/auth/oauth2/token",
  "introspection_endpoint": "https://auth.myapp.com/v1/auth/oauth2/introspect",
  "revocation_endpoint": "https://auth.myapp.com/v1/auth/oauth2/revoke",
  "jwks_uri": "https://auth.myapp.com/.well-known/jwks.json",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid", "profile", "email", "phone", "offline_access"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
  "code_challenge_methods_supported": ["S256"]
}

Configuration reference

OptionTypeDefaultDescription
EnabledboolfalseEnable OAuth2 provider functionality
Issuerstring""Issuer identifier URL (must be HTTPS in production)
AccessTokenTTLstring"1h"Access token lifetime
RefreshTokenTTLstring"30d"Refresh token lifetime
AuthorizationCodeTTLstring"10m"Authorization code validity
RequirePKCEbooltrueRequire PKCE for all authorization code grants
AllowedScopes[]string["openid", "profile", "email"]Scopes that clients can request

API routes

MethodPathDescription
GET/oauth2/authorizeAuthorization endpoint (redirects user)
POST/oauth2/tokenToken endpoint (code exchange, client credentials, refresh)
POST/oauth2/introspectToken introspection
POST/oauth2/revokeToken revocation
POST/oauth2/clientsRegister a new OAuth2 client
GET/oauth2/clientsList registered clients
GET/oauth2/clients/:clientIdGet client details
PATCH/oauth2/clients/:clientIdUpdate a client
DELETE/oauth2/clients/:clientIdDelete a client
GET/.well-known/jwks.jsonJWKS endpoint
GET/.well-known/openid-configurationOIDC discovery document

On this page