Oauthtoothy

Complete OAuth2 authentication middleware for CHICKEN Scheme web applications.

Description

Oauthtoothy is a configurable OAuth2 authentication middleware that provides complete OAuth2 authentication flow integration for web applications. It handles OAuth2 authorization, token exchange, user info retrieval, and session management automatically. The middleware registers routes for each provider and manages authentication state across requests.

Requirements

Installation

chicken-install oauthtoothy

Basic Usage

(import schematra schematra-session oauthtoothy chiccup)

;; Define user data parser for Google
(define (parse-google-user json-response)
  `((id . ,(alist-ref 'id json-response))
    (name . ,(alist-ref 'name json-response))
    (email . ,(alist-ref 'email json-response))))

;; Configure OAuth2 provider
(define google-provider
  `((name . "google")
    (client-id . ,(get-environment-variable "GOOGLE_CLIENT_ID"))
    (client-secret . ,(get-environment-variable "GOOGLE_CLIENT_SECRET"))
    (auth-url . "https://accounts.google.com/o/oauth2/auth")
    (token-url . "https://oauth2.googleapis.com/token")
    (user-info-url . "https://www.googleapis.com/oauth2/v2/userinfo")
    (scopes . "profile email")
    (user-info-parser . ,parse-google-user)))

;; Install middleware
(use-middleware! (session-middleware "secret-key"))
(use-middleware!
 (oauthtoothy-middleware
  (list google-provider)
  success-redirect: "/profile"))

;; Protected route
(get ("/profile")
     (let ((auth (current-auth)))
       (if (alist-ref 'authenticated? auth)
           (ccup->html `[h1 ,(string-append "Welcome, " (alist-ref 'name auth))])
           (redirect "/auth/google"))))

(schematra-install)
(schematra-start)

API

[procedure] (oauthtoothy-middleware providers #!key success-redirect save-proc load-proc)

Creates OAuth2 authentication middleware for Schematra applications.

Parameters:

;; Basic setup
(oauthtoothy-middleware (list google-provider))

;; With custom redirect and persistence
(oauthtoothy-middleware
 (list google-provider github-provider)
 success-redirect: "/dashboard"
 save-proc: save-user-to-db
 load-proc: load-user-from-db)

Registered Routes

For each provider named "example", the middleware automatically registers:

[parameter] (auth-base-url [url])

Base URL parameter for OAuth2 callback URLs. Default: "http://localhost:8080".

This URL must be registered with your OAuth2 providers as an allowed callback URL. The actual callback path will be "{base-url}/auth/{provider}/callback".

;; For production deployment
(auth-base-url "https://myapp.example.com")

;; For local development with custom port
(auth-base-url "http://localhost:3000")
[parameter] (current-auth)

Current authentication state parameter containing user authentication details.

Unauthenticated state: ((authenticated? . #f))

Authenticated state structure:

;; Check authentication in route handlers
(get ("/protected")
     (let ((auth (current-auth)))
       (if (alist-ref 'authenticated? auth)
           (format "Welcome, ~A!" (alist-ref 'name auth))
           (redirect "/login"))))

;; Access user data
(let ((auth (current-auth)))
  (when (alist-ref 'authenticated? auth)
    (let ((user-id (alist-ref 'user-id auth))
          (email (alist-ref 'email auth)))
      ;; Process authenticated user...
      )))

Provider Configuration

Each provider must be an association list with these required keys:

Required Keys:

;; Google OAuth2 provider
(define google-provider
  `((name . "google")
    (client-id . "your-google-client-id")
    (client-secret . "your-google-client-secret")
    (auth-url . "https://accounts.google.com/o/oauth2/auth")
    (token-url . "https://oauth2.googleapis.com/token")
    (user-info-url . "https://www.googleapis.com/oauth2/v2/userinfo")
    (scopes . "profile email")
    (user-info-parser . ,parse-google-user)))

;; GitHub OAuth2 provider
(define github-provider
  `((name . "github")
    (client-id . "your-github-client-id")
    (client-secret . "your-github-client-secret")
    (auth-url . "https://github.com/login/oauth/authorize")
    (token-url . "https://github.com/login/oauth/access_token")
    (user-info-url . "https://api.github.com/user")
    (scopes . "read:user user:email")
    (user-info-parser . ,parse-github-user)))

User Info Parser

The user-info-parser function normalizes provider-specific user data into a consistent format:

;; Google user parser
(define (parse-google-user json-response)
  `((id . ,(alist-ref 'id json-response))
    (name . ,(alist-ref 'name json-response))
    (email . ,(alist-ref 'email json-response))
    (avatar . ,(alist-ref 'picture json-response))))

;; GitHub user parser
(define (parse-github-user json-response)
  `((id . ,(number->string (alist-ref 'id json-response)))
    (name . ,(alist-ref 'name json-response))
    (email . ,(alist-ref 'email json-response))
    (username . ,(alist-ref 'login json-response))
    (avatar . ,(alist-ref 'avatar_url json-response))))

Persistence

For user data persistence beyond sessions, provide save-proc and load-proc functions:

;; Save user data to database
(define (save-user-to-db user-id user-data token)
  (execute-sql "INSERT OR REPLACE INTO users (id, name, email, avatar) VALUES (?, ?, ?, ?)"
               user-id
               (alist-ref 'name user-data)
               (alist-ref 'email user-data)
               (alist-ref 'avatar user-data)))

;; Load user data from database
(define (load-user-from-db user-id)
  (let ((row (query-row "SELECT name, email, avatar FROM users WHERE id = ?" user-id)))
    (if row
        `((name . ,(first row))
          (email . ,(second row))
          (avatar . ,(third row)))
        #f)))

;; Use with middleware
(oauthtoothy-middleware
 (list google-provider)
 save-proc: save-user-to-db
 load-proc: load-user-from-db)

Complete Examples

Multi-Provider Authentication

(import schematra schematra-session oauthtoothy chiccup)

;; User parsers
(define (parse-google-user json-response)
  `((id . ,(alist-ref 'id json-response))
    (name . ,(alist-ref 'name json-response))
    (email . ,(alist-ref 'email json-response))
    (provider . "google")))

(define (parse-github-user json-response)
  `((id . ,(number->string (alist-ref 'id json-response)))
    (name . ,(alist-ref 'name json-response))
    (username . ,(alist-ref 'login json-response))
    (provider . "github")))

;; Provider configurations
(define providers
  (list
   `((name . "google")
     (client-id . ,(get-environment-variable "GOOGLE_CLIENT_ID"))
     (client-secret . ,(get-environment-variable "GOOGLE_CLIENT_SECRET"))
     (auth-url . "https://accounts.google.com/o/oauth2/auth")
     (token-url . "https://oauth2.googleapis.com/token")
     (user-info-url . "https://www.googleapis.com/oauth2/v2/userinfo")
     (scopes . "profile email")
     (user-info-parser . ,parse-google-user))
   `((name . "github")
     (client-id . ,(get-environment-variable "GITHUB_CLIENT_ID"))
     (client-secret . ,(get-environment-variable "GITHUB_CLIENT_SECRET"))
     (auth-url . "https://github.com/login/oauth/authorize")
     (token-url . "https://github.com/login/oauth/access_token")
     (user-info-url . "https://api.github.com/user")
     (scopes . "read:user user:email")
     (user-info-parser . ,parse-github-user))))

;; Install middleware
(use-middleware! (session-middleware "your-secret-key"))
(use-middleware! (oauthtoothy-middleware providers success-redirect: "/profile"))

;; Login page with provider options
(get ("/login")
     (ccup->html
      `[.login-page
        [h1 "Login"]
        [.providers
         [a.btn (@ (href "/auth/google")) "Login with Google"]
         [a.btn (@ (href "/auth/github")) "Login with GitHub"]]]))

;; Protected profile page
(get ("/profile")
     (let ((auth (current-auth)))
       (if (alist-ref 'authenticated? auth)
           (ccup->html
            `[.profile
              [h1 ,(format "Welcome, ~A!" (alist-ref 'name auth))]
              [p ,(format "Provider: ~A" (alist-ref 'provider auth))]
              [a (@ (href "/logout")) "Logout"]])
           (redirect "/login"))))

;; Logout
(get ("/logout")
     (session-destroy!)
     (redirect "/"))

(schematra-install)
(schematra-start)

Database Integration

(import sql-de-lite)

;; Database setup
(define db (open-database "app.db"))
(execute-sql db "CREATE TABLE IF NOT EXISTS users (
                   id TEXT PRIMARY KEY,
                   name TEXT,
                   email TEXT,
                   provider TEXT,
                   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                 )")

;; Persistence functions
(define (save-user user-id user-data token)
  (execute-sql db "INSERT OR REPLACE INTO users (id, name, email, provider) VALUES (?, ?, ?, ?)"
               user-id
               (alist-ref 'name user-data)
               (alist-ref 'email user-data)
               (alist-ref 'provider user-data)))

(define (load-user user-id)
  (let ((row (query-row db "SELECT name, email, provider FROM users WHERE id = ?" user-id)))
    (if row
        `((name . ,(first row))
          (email . ,(second row))
          (provider . ,(third row)))
        #f)))

;; Install middleware with persistence
(use-middleware!
 (oauthtoothy-middleware
  providers
  success-redirect: "/profile"
  save-proc: save-user
  load-proc: load-user))

Security Considerations

Session Integration

Oauthtoothy requires session middleware to be installed first. It stores the user-id in the session and optionally uses save-proc/load-proc for additional user data persistence.

;; Session middleware must be installed before oauthtoothy
(use-middleware! (session-middleware "your-secret-key"))
(use-middleware! (oauthtoothy-middleware providers))

;; Session contains user-id after authentication
(session-get "user-id")  ; => user's unique identifier

License

Copyright © 2025 Rolando Abarca. Released under BSD-3-Clause License.

Repository

GitHub Repository