diff --git a/auth/oauth2.go b/auth/oauth2.go new file mode 100644 index 0000000..8cf37dd --- /dev/null +++ b/auth/oauth2.go @@ -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) +} diff --git a/auth/url.go b/auth/url.go index e5b2f28..ed8a7ac 100644 --- a/auth/url.go +++ b/auth/url.go @@ -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) } diff --git a/doc/tokidoki.8.scd b/doc/tokidoki.8.scd index 528c6d8..cf2220f 100644 --- a/doc/tokidoki.8.scd +++ b/doc/tokidoki.8.scd @@ -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 diff --git a/go.mod b/go.mod index 33042be..7a551ad 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c585855..57b99a6 100644 --- a/go.sum +++ b/go.sum @@ -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=