---
auth/oauth2.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++
auth/url.go | 8 +++++
doc/tokidoki.8.scd | 7 ++++
go.mod | 1 +
go.sum | 2 ++
5 files changed, 105 insertions(+)
create mode 100644 auth/oauth2.go
diff --git a/auth/oauth2.go b/auth/oauth2.go
new file mode 100644
index 000000000000..8cf37dd6aca2
--- /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 e5b2f283cef4..ed8a7aca793c 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 528c6d84b296..cf2220f1a8fa 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 33042bec1263..7a551ad76ef7 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 c58585544c95..57b99a61b6e9 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=
base-commit: cca1d579db10279811e3802fa8b900bff9d7a634
--
2.43.2
Quite interesting! Thanks!
to git@git.sr.ht:~sircmpwn/tokidoki
cca1d57..ebb5aed master -> master