Quellcode durchsuchen

feature: OAuth2! :-D

jamesread vor 1 Jahr
Ursprung
Commit
6a7187fb5b

+ 9 - 8
go.mod

@@ -21,6 +21,7 @@ require (
 	github.com/spf13/viper v1.15.0
 	github.com/stretchr/testify v1.9.0
 	golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
+	golang.org/x/oauth2 v0.23.0
 	google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa
 	google.golang.org/grpc v1.62.1
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
@@ -118,16 +119,16 @@ require (
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
-	golang.org/x/crypto v0.21.0 // indirect
+	golang.org/x/crypto v0.25.0 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225 // indirect
-	golang.org/x/mod v0.16.0 // indirect
-	golang.org/x/net v0.23.0 // indirect
-	golang.org/x/sync v0.6.0 // indirect
-	golang.org/x/sys v0.18.0 // indirect
-	golang.org/x/term v0.18.0 // indirect
-	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/mod v0.17.0 // indirect
+	golang.org/x/net v0.27.0 // indirect
+	golang.org/x/sync v0.7.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	golang.org/x/term v0.22.0 // indirect
+	golang.org/x/text v0.16.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
-	golang.org/x/tools v0.19.0 // indirect
+	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 )

+ 18 - 16
go.sum

@@ -410,8 +410,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -451,8 +451,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
-golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -484,8 +484,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -495,6 +495,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -505,8 +507,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -548,19 +550,19 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
-golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
+golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -615,8 +617,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
-golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

+ 13 - 0
internal/config/config.go

@@ -117,6 +117,7 @@ type Config struct {
 	AuthHttpHeaderUserGroup         string
 	AuthLoginUrl                    string
 	AuthAllowGuest                  bool
+	AuthOAuth2Providers             map[string]*OAuth2Provider
 	DefaultPermissions              PermissionsList
 	AccessControlLists              []*AccessControlList
 	WebUIDir                        string
@@ -137,6 +138,18 @@ type Config struct {
 	usedConfigDir string
 }
 
+type OAuth2Provider struct {
+	Name          string
+	ClientID      string
+	ClientSecret  string
+	Icon          string
+	Scopes        []string
+	AuthUrl       string
+	TokenUrl      string
+	WhoamiUrl     string
+	UsernameField string
+}
+
 type NavigationLink struct {
 	Title string
 	Url   string

+ 249 - 0
internal/httpservers/oauth2.go

@@ -0,0 +1,249 @@
+package httpservers
+
+import (
+	"context"
+	"crypto/rand"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	config "github.com/OliveTin/OliveTin/internal/config"
+	log "github.com/sirupsen/logrus"
+	"golang.org/x/oauth2"
+	"io"
+	"net/http"
+	"time"
+)
+
+var (
+	registeredStates    = make(map[string]*oauth2State)
+	registeredProviders = make(map[string]*oauth2.Config)
+)
+
+type oauth2State struct {
+	provider  *oauth2.Config
+	Username  string
+	Usergroup string
+}
+
+func assignIfEmpty(target *string, value string) {
+	if *target == "" {
+		*target = value
+	}
+}
+
+func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
+	dbConfig, ok := oauth2ProviderDatabase[providerName]
+
+	if ok {
+		assignIfEmpty(&providerConfig.WhoamiUrl, dbConfig.WhoamiUrl)
+		assignIfEmpty(&providerConfig.TokenUrl, dbConfig.TokenUrl)
+		assignIfEmpty(&providerConfig.AuthUrl, dbConfig.AuthUrl)
+		assignIfEmpty(&providerConfig.Icon, dbConfig.Icon)
+		assignIfEmpty(&providerConfig.UsernameField, dbConfig.UsernameField)
+
+		if providerConfig.Scopes == nil {
+			providerConfig.Scopes = dbConfig.Scopes
+		}
+	} else {
+		log.Warnf("Provider not found in database: %v", providerName)
+	}
+}
+
+func getOAuth2Config(cfg *config.Config, providerName string) (*oauth2.Config, error) {
+	config, ok := registeredProviders[providerName]
+
+	if !ok {
+		providerConfig, ok := cfg.AuthOAuth2Providers[providerName]
+
+		if !ok {
+			return nil, fmt.Errorf("Provider not found in config: %v", providerName)
+		}
+
+		completeProviderConfig(providerName, providerConfig)
+
+		config = &oauth2.Config{
+			ClientID:     providerConfig.ClientID,
+			ClientSecret: providerConfig.ClientSecret,
+			Scopes:       providerConfig.Scopes,
+			Endpoint: oauth2.Endpoint{
+				AuthURL:  providerConfig.AuthUrl,
+				TokenURL: providerConfig.TokenUrl,
+			},
+			RedirectURL: "http://localhost:1337/oauth/callback",
+		}
+
+		registeredProviders[providerName] = config
+
+		log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
+	}
+
+	return config, nil
+}
+
+func randString(nByte int) (string, error) {
+	b := make([]byte, nByte)
+
+	if _, err := io.ReadFull(rand.Reader, b); err != nil {
+		return "", err
+	}
+
+	return base64.URLEncoding.EncodeToString(b), nil
+}
+
+func setOauthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
+	cookie := &http.Cookie{
+		Name:     name,
+		Value:    value,
+		MaxAge:   int(time.Hour.Seconds()),
+		Secure:   r.TLS != nil,
+		HttpOnly: true,
+		Path:     "/",
+	}
+
+	http.SetCookie(w, cookie)
+}
+
+func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
+	state, err := randString(16)
+
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	providerName := r.URL.Query().Get("provider")
+	provider, err := getOAuth2Config(cfg, providerName)
+
+	registeredStates[state] = &oauth2State{
+		provider: provider,
+	}
+
+	if err != nil {
+		log.Errorf("Failed to get provider config: %v %v", providerName, err)
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	setOauthCallbackCookie(w, r, "oauth2state", state)
+
+	log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
+
+	http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
+}
+
+func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, bool) {
+	state, err := r.Cookie("oauth2state")
+
+	if err != nil {
+		log.Errorf("Failed to get state cookie: %v", err)
+
+		http.Error(w, "State not found", http.StatusBadRequest)
+		return nil, false
+	}
+
+	if r.URL.Query().Get("state") != state.Value {
+		log.Errorf("State mismatch: %v != %v", r.URL.Query().Get("state"), state.Value)
+
+		http.Error(w, "State mismatch", http.StatusBadRequest)
+		return nil, false
+	}
+
+	registeredState, ok := registeredStates[state.Value]
+
+	if !ok {
+		log.Errorf("State not found in server: %v", state.Value)
+
+		http.Error(w, "State not found in server", http.StatusBadRequest)
+	}
+
+	return registeredState, true
+}
+
+func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
+	log.Infof("OAuth2 Callback received")
+
+	registeredState, ok := checkOAuthCallbackCookie(w, r)
+
+	if !ok {
+		return
+	}
+
+	code := r.FormValue("code")
+
+	log.Debugf("OAuth2 Token Code: %v", code)
+
+	httpClient := &http.Client{Timeout: 2 * time.Second}
+	ctx := context.Background()
+	ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
+
+	tok, err := registeredState.provider.Exchange(ctx, code)
+
+	if err != nil {
+		log.Errorf("Failed to exchange code: %v", err)
+		http.Error(w, "Failed to exchange code", http.StatusBadRequest)
+		return
+	}
+
+	client := registeredState.provider.Client(ctx, tok)
+
+	registeredState.Username = getUsername(client)
+
+	loginMessage := fmt.Sprintf("Logged in as %v", registeredState.Username)
+
+	log.Infof(loginMessage)
+
+	w.Write([]byte(loginMessage))
+}
+
+func getUsername(client *http.Client) string {
+	provider := cfg.AuthOAuth2Providers["github"]
+
+	res, err := client.Get(provider.WhoamiUrl)
+
+	if res.StatusCode != http.StatusOK {
+		log.Errorf("Failed to get user data: %v", res.StatusCode)
+		return ""
+	}
+
+	defer res.Body.Close()
+
+	contents, err := io.ReadAll(res.Body)
+
+	var userData map[string]interface{}
+
+	err = json.Unmarshal([]byte(contents), &userData)
+
+	if err != nil {
+		log.Errorf("Failed to unmarshal user data: %v", err)
+
+		return ""
+	}
+
+	username, ok := userData[provider.UsernameField]
+
+	if !ok {
+		log.Errorf("Failed to get username from user data: %v", userData)
+
+		return ""
+	}
+
+	return username.(string)
+}
+
+func parseOAuth2Cookie(r *http.Request) (string, string) {
+	cookie, err := r.Cookie("oauth2state")
+
+	if err != nil {
+		log.Warnf("Failed to read OAuth2 cookie: %v", err)
+		return "", ""
+	}
+
+	serverState, found := registeredStates[cookie.Value]
+
+	if !found {
+		log.Warnf("Failed to find OAuth2 state: %v", cookie.Value)
+		return "", ""
+	}
+
+	return serverState.Username, serverState.Usergroup
+}

+ 24 - 0
internal/httpservers/oauth2_providers.go

@@ -0,0 +1,24 @@
+package httpservers
+
+import (
+	config "github.com/OliveTin/OliveTin/internal/config"
+	"golang.org/x/oauth2/endpoints"
+)
+
+var oauth2ProviderDatabase = map[string]config.OAuth2Provider{
+	"github": {
+		Icon:          "github",
+		WhoamiUrl:     "https://api.github.com/user",
+		TokenUrl:      endpoints.GitHub.TokenURL,
+		AuthUrl:       endpoints.GitHub.AuthURL,
+		Scopes:        []string{"profile", "email"},
+		UsernameField: "login",
+	},
+	"google": {
+		Icon:      "google",
+		WhoamiUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
+		TokenUrl:  endpoints.Google.TokenURL,
+		AuthUrl:   endpoints.Google.AuthURL,
+		Scopes:    []string{"profile", "email"},
+	},
+}

+ 4 - 0
internal/httpservers/restapi.go

@@ -57,6 +57,10 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
 		username, usergroup = parseHttpHeaderForAuth(req)
 	}
 
+	if len(cfg.AuthOAuth2Providers) > 0 {
+		username, usergroup = parseOAuth2Cookie(req)
+	}
+
 	md := metadata.New(map[string]string{
 		"username":  username,
 		"usergroup": usergroup,

+ 4 - 0
internal/httpservers/singleFrontend.go

@@ -56,6 +56,10 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
 		websocket.HandleWebsocket(w, r)
 	})
 
+	mux.HandleFunc("/oauth/login", handleOAuthLogin)
+
+	mux.HandleFunc("/oauth/callback", handleOAuthCallback)
+
 	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		logDebugRequest(cfg, "ui  ", r)