~sircmpwn/tokidoki

ebb5aede924e3699cfb012b04b7e9d014a839d3f — Simon Ser 3 months ago cca1d57
Add OAuth 2.0 backend
5 files changed, 105 insertions(+), 0 deletions(-)

A auth/oauth2.go
M auth/url.go
M doc/tokidoki.8.scd
M go.mod
M go.sum
A auth/oauth2.go => auth/oauth2.go +87 -0
@@ 0,0 1,87 @@
package auth

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/rs/zerolog/log"

	"git.sr.ht/~emersion/go-oauth2"
)

type OAuth2Provider struct {
	metadata     *oauth2.ServerMetadata
	clientID     string
	clientSecret string
}

// Initializes a new OAuth 2.0 auth provider with the given connection string.
func NewOAuth2(endpoint, clientID, clientSecret string) (AuthProvider, error) {
	metadata, err := oauth2.DiscoverServerMetadata(context.Background(), endpoint)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch OAuth 2.0 server metadata: %v", err)
	}
	return &OAuth2Provider{
		metadata:     metadata,
		clientID:     clientID,
		clientSecret: clientSecret,
	}, nil
}

func (prov *OAuth2Provider) Middleware() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			prov.doAuth(next, w, r)
		})
	}
}

func (prov *OAuth2Provider) doAuth(next http.Handler,
	w http.ResponseWriter, r *http.Request) {

	auth := r.Header.Get("Authorization")
	authScheme, creds, _ := strings.Cut(auth, " ")
	var username, accessToken string
	switch strings.ToLower(authScheme) {
	case "bearer":
		accessToken = creds
	case "basic":
		username, accessToken, _ = r.BasicAuth()
	default:
		w.Header().Add("WWW-Authenticate", `Bearer, Basic realm="Please provide an OAuth access token", charset="UTF-8"`)
		http.Error(w, "HTTP auth is required", http.StatusUnauthorized)
		return
	}

	client := oauth2.Client{
		Server:       prov.metadata,
		ClientID:     prov.clientID,
		ClientSecret: prov.clientSecret,
	}
	resp, err := client.Introspect(r.Context(), accessToken)
	if err != nil || !resp.Active {
		log.Debug().Err(err).Msg("auth error")
		http.Error(w, "Invalid access token", http.StatusUnauthorized)
		return
	}

	if username != "" && username != resp.Username {
		http.Error(w, "Invalid username", http.StatusUnauthorized)
		return
	}

	if resp.Username == "" {
		http.Error(w, "OAuth 2.0 server did not send username", http.StatusInternalServerError)
		return
	}

	authCtx := AuthContext{
		AuthMethod: "oauth2",
		UserName:   resp.Username,
	}
	ctx := NewContext(r.Context(), &authCtx)
	r = r.WithContext(ctx)
	next.ServeHTTP(w, r)
}

M auth/url.go => auth/url.go +8 -0
@@ 26,6 26,14 @@ func NewFromURL(authURL string) (AuthProvider, error) {
		return NewHtpasswd(path)
	case "null":
		return NewNull()
	case "http", "https":
		if u.User == nil {
			return nil, fmt.Errorf("missing client ID for OAuth 2.0")
		}
		clientID := u.User.Username()
		clientSecret, _ := u.User.Password()
		u.User = nil
		return NewOAuth2(u.String(), clientID, clientSecret)
	default:
		return nil, fmt.Errorf("no auth provider found for %s:// URL", u.Scheme)
	}

M doc/tokidoki.8.scd => doc/tokidoki.8.scd +7 -0
@@ 74,6 74,13 @@ URL: *pam://* (no parameters)
_Note:_ The PAM auth backend must be enabled at build time, as PAM may not be
available on all platforms.

## OAuth 2.0

The OAuth 2.0 auth backend delegates authentication to the provided OAuth 2.0
server.

URL: *https://*_client_id_*:*_client_secret_*@*_host_

## Static file (htpasswd)

The static file auth backend relies on the file format popularized by Apache and

M go.mod => go.mod +1 -0
@@ 3,6 3,7 @@ module git.sr.ht/~sircmpwn/tokidoki
go 1.18

require (
	git.sr.ht/~emersion/go-oauth2 v0.0.0-20240217160856-2e0d6e20b088
	github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f
	github.com/emersion/go-imap v1.2.1
	github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43

M go.sum => go.sum +2 -0
@@ 1,3 1,5 @@
git.sr.ht/~emersion/go-oauth2 v0.0.0-20240217160856-2e0d6e20b088 h1:KuPliLD8CQM1WbCHdjHR6mhadIzLaAJCNENmvB1y9gs=
git.sr.ht/~emersion/go-oauth2 v0.0.0-20240217160856-2e0d6e20b088/go.mod h1:VHj0jSCLIkrfEwmOvJ4+ykpoVbD/YLN7BM523oKKBHc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ=
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=