Skip to content

Use case — Minimal OP and RP pair ​

This example gets an Authorization Code + PKCE round-trip working with the shortest option list the library accepts. It starts the OP and a paired RP (the application that signs users in through it) in one process, so a browser can drive the whole flow with nothing else installed.

Source: examples/01-minimal/main.go

Architecture ​

One Go process, nothing else running
A browser drives the bundled relying party, which speaks OIDC to the provider in the same process. The provider reads and writes an in-memory store and signs with an ephemeral keyset — no database and no key management to set up.Browseryoursa single Go processrpkit RP:9090OIDCop.New(…):8080inmem.Storegone on restartKeysetephemeral ECDSA P-256
Both boxes on the right are deliberately throwaway. There is no database, no key management and no configuration file: everything inside the frame is created at start-up, and restarting loses every session and every key, which is exactly what you want while you are still finding out whether the flow works at all.

The store is in-memory and the keys are generated at boot. The example seeds one demo user (demo / demo) and registers a public client whose redirect_uri points back at the embedded RP.

Code (essentials) ​

go
package main

import (
  "log"
  "log/slog"
  "net/http"

  "github.com/libraz/go-oidc-provider/op"
  "github.com/libraz/go-oidc-provider/op/storeadapter/inmem"
)

func main() {
  keys := /* devkeys.MustEphemeral("minimal-1") in the example */
  st := inmem.New()
  // seedUser hashes "demo"/"demo" with op.HashPassword and stores a
  // *store.User through st.PutUserWithPassword; see the example for the body.

  // One Primary step and no rules is the whole of a password-only login.
  flow := op.LoginFlow{
    Primary: op.PrimaryPassword{Store: st.UserPasswords()},
  }

  provider, err := op.New(
    op.WithIssuer("http://127.0.0.1:8080"),
    op.WithStore(st),
    op.WithKeyset(keys.Keyset()),
    op.WithCookieKeys(keys.CookieKey),
    op.WithLogger(slog.Default()),
    op.WithLoginFlow(flow),
    op.WithStaticClients(op.PublicClient{
      ID:           "demo-rp",
      RedirectURIs: []string{"http://127.0.0.1:9090/callback"},
      Scopes:       []string{"openid", "profile"},
    }),
  )
  if err != nil {
    log.Fatalf("op.New: %v", err)
  }

  mux := http.NewServeMux()
  mux.Handle("/", provider)
  log.Fatal(http.ListenAndServe(":8080", mux))
}

WithIssuer, WithStore, and WithKeyset are enough for /.well-known/openid-configuration and /oidc/jwks to answer. WithCookieKeys and WithLoginFlow are required once the authorization-code grant is enabled, which it is by default, so a runnable OP takes five inputs. WithLogger is not required, but without it the OP discards request-time logs and audit records and warns about it at start-up. No op.WithProfile is declared, so the OP runs profile.OAuth21 and every authorization-code request needs PKCE; the bundled RP sends it (see Declaring a security profile). Anything that involves a user (authorize, token, userinfo) also needs at least one client (WithStaticClients). Getting started: minimal OP walks through why each required option exists.

What the OP exposes ​

The defaults mount under /oidc (override with op.WithMountPrefix):

PathPurpose
/.well-known/openid-configurationDiscovery (always at the root, OIDC Discovery 1.0 §4)
/oidc/jwksPublic JWKS for ID Token / JWT access token verification
/oidc/authAuthorization endpoint
/oidc/tokenToken endpoint
/oidc/userinfoUserInfo (RFC 6749 + OIDC Core §5.3)
/oidc/end_sessionRP-Initiated Logout 1.0

The optional endpoints (/par, /introspect, /revoke, /register and the like) mount only when their feature or grant is enabled.

What's missing for a real deployment ​

GapFix
Single demo user is hard-codedEnrol users through your own management plane, or project your existing user table (Bring your own user store).
Ephemeral keys: ID Tokens become unverifiable after a restartLoad keys from a vault, KMS or file.
In-memory store: all state is lost on restartSwitch to op/storeadapter/sql or op/storeadapter/composite (Persistent storage).
Plain HTTP listener (http://127.0.0.1)Front behind a TLS-terminating ingress; switch issuer to https://.
Single-factor (password only)Add RuleAlways(StepTOTP{...}) — see MFA / step-up.
Demo RP code in examples/internal/rpkitProduction RPs use golang.org/x/oauth2 + github.com/coreos/go-oidc/v3 directly.

examples/02-bundle closes some of these gaps; see Comprehensive bundle.

Run it ​

sh
git clone https://github.com/libraz/go-oidc-provider.git
cd go-oidc-provider
(cd examples/01-minimal && GOWORK=off go run -tags example .)

Then, in a browser:

  1. Open http://127.0.0.1:9090/ (the RP) and choose "Log in via the OP".
  2. Sign in as demo / demo, then approve the consent prompt.
  3. The RP lands on /me and shows the verified ID Token claims; iss is http://127.0.0.1:8080.

To see the OP's own metadata, curl -s http://127.0.0.1:8080/.well-known/openid-configuration | jq. If the round-trip stalls, restart the process: the keys are ephemeral, so every restart invalidates in-flight sessions.