Skip to content

dpella/oauth2-server

Repository files navigation

oauth2-server — OAuth 2.1 Authorization Server for Servant

Hackage

oauth2-server is a small, composable OAuth 2.1 authorization server for Haskell/Servant. It implements the core endpoints for authorization code with PKCE, dynamic client registration, token issuance and refresh, and discovery metadata. It integrates with servant-auth-server to mint JWT access tokens and lets you plug in your own username/password authentication via a simple typeclass.

This library is designed to be embedded inside your existing Servant application, mounting the OAuth routes alongside your APIs.

Features

  • OAuth 2.1 authorization code flow with PKCE (RFC 6749 + RFC 7636)
  • Token endpoint with refresh token rotation
  • Dynamic client registration (RFC 7591)
  • Authorization server metadata discovery (RFC 8414)
  • JWT access tokens via servant-auth-server
  • Pluggable user authentication through a FormAuth typeclass
  • In‑memory refresh token persistence by default, with an interface to plug your own store

Endpoints

  • GET /.well-known/oauth-authorization-server — discovery metadata
  • GET /authorize — start authorization (renders a login form)
  • POST /authorize/callback — handles login form submission and issues authorization codes
  • POST /token — exchanges codes for tokens and refreshes tokens
  • POST /register — dynamic client registration

PKCE is enforced for all clients using the authorization code grant. Use either S256 or plain as the challenge method.

Install

Add the package to your library or executable stanza:

build-depends:
    base
  , servant
  , servant-server
  , servant-auth-server
  , blaze-html
  , text
  , aeson
  , containers
  , oauth2-server  -- this package

This project targets GHC 9.12 (see cabal.project).

Quick Start

Below is a minimal Servant application that mounts the OAuth server. It defines a user type, a simple FormAuth instance, configures JWT settings, initializes the OAuth state, and serves the combined OAuth API.

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module Main where

import Control.Concurrent.MVar (newMVar)
import Data.Text (Text)
import GHC.Generics (Generic)
import Network.Wai (Application)
import Network.Wai.Handler.Warp (run)
import Servant
import Servant.Auth.Server
import Web.OAuth2 (OAuthAPI, oAuthAPI, defaultLoginFormRenderer)
import Web.OAuth2.Types

-- Your application user type and JWT instances
data User = User { userId :: Text } deriving (Show, Generic)
instance ToJWT User
instance FromJWT User

-- Plug in your authentication (username/password) logic
data MyAuthSettings = MyAuthSettings
instance FormAuth User where
  type FormAuthSettings User = MyAuthSettings
  runFormAuth _ "alice" "wonderland" = pure (Authenticated (User "alice"))
  runFormAuth _ _ _ = pure NoSuchUser

type Ctx = '[JWTSettings, MyAuthSettings]

mkApp :: IO Application
mkApp = do
  jwk <- generateKey
  let jwt = defaultJWTSettings jwk
      ctx = jwt :. MyAuthSettings :. EmptyContext

  -- In-memory refresh-token persistence (swap for your DB if needed)
  rtp <- mkDefaultRefreshTokenPersistence
  st  <- newMVar (initOAuthState @User "http://localhost" 8080 rtp defaultLoginFormRenderer)

  pure $ serveWithContext (Proxy :: Proxy OAuthAPI) ctx (oAuthAPI st ctx)

main :: IO ()
main = mkApp >>= run 8080

Notes:

  • initOAuthState sets the base URL and port used in discovery metadata.
  • Pass defaultLoginFormRenderer for the built-in login page, or supply your own LoginFormParams -> Html function to customise the look-and-feel.
  • For production, provide a durable RefreshTokenPersistence (e.g. database) by implementing persistRefreshToken, deleteRefreshToken, and lookupRefreshToken.
  • The default login page references /static/logo.png if present; it's optional.

Short Tutorial

This walkthrough registers a client, runs a PKCE authorization code flow, exchanges the code for tokens, and refreshes the token. Replace placeholders as needed.

  1. Register a client
curl -s http://localhost:8080/register \
  -H 'Content-Type: application/json' \
  -d '{
    "client_name": "My App",
    "redirect_uris": ["http://localhost:3000/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "read write",
    "token_endpoint_auth_method": "none"
  }'
# => { "client_id": "client_...", ... }
  1. Start authorization (PKCE)

For a simple demo, use code_challenge_method=plain and set both challenge and verifier to the same value, e.g. testverifier.

Open in your browser:

http://localhost:8080/authorize?response_type=code&client_id=CLIENT_ID\
&redirect_uri=http://localhost:3000/callback&scope=read&state=xyz\
&code_challenge=testverifier&code_challenge_method=plain

Log in with the credentials handled by your FormAuth instance (from the example above: username alice, password wonderland). You will be redirected to the redirect_uri with ?code=...&state=xyz.

  1. Exchange the code for tokens
curl -s http://localhost:8080/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:3000/callback&client_id=CLIENT_ID&code_verifier=testverifier"
# => { "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "...", "scope": "read" }
  1. Refresh the access token (rotation)
curl -s http://localhost:8080/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d "grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=CLIENT_ID"
# => { "access_token": "...", "refresh_token": "..." }
  1. Discover server metadata (optional)
curl -s http://localhost:8080/.well-known/oauth-authorization-server | jq

Key Types and APIs

  • OAuthAPI — combined Servant API for all OAuth endpoints
  • oAuthAPI :: MVar (OAuthState usr) -> Context ctxt -> Server OAuthAPI — server implementation
  • FormAuth usr — plug‑in credential verification for your user type:
    • associated type FormAuthSettings usr
    • runFormAuth :: Context ctxt -> Text -> Text -> IO (AuthResult usr)
  • OAuthState usr — holds authorization codes, client registry, refresh‑token persistence, and login form renderer
  • mkDefaultRefreshTokenPersistence — in‑memory RefreshToken storage (replace in production)
  • initOAuthState — construct initial state with base URL, port, and login form renderer
  • defaultLoginFormRenderer — built-in login page; pass to initOAuthState or replace with your own
  • LoginFormParams — parameters available to a custom login form renderer

See also:

  • tests/OAuth/FlowSpec.hs — end‑to‑end flow covering registration, authorization, token exchange, and refresh
  • tests/OAuth/TestUtils.hs — example FormAuth instance and JWT configuration

Development

Run tests:

cabal build
cabal test

Security Notes

  • Always serve over HTTPS in production. Set oauth_url accordingly.
  • Use a strong JWT signing key and appropriate token lifetimes.
  • PKCE is required for authorization code exchanges. Prefer S256 in real clients.
  • The default refresh‑token store is in‑memory and not durable; implement your own for production.

License

See LICENSE in this repository.

About

OAuth 2.1 authorization server implementation

Resources

License

Stars

Watchers

Forks

Packages

No packages published