From 1621683fdbb808bc23c377448bdab498161efefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=9CL=C3=96P?= Date: Sun, 19 Feb 2023 11:39:06 +0000 Subject: [PATCH] feat: auth method oidc --- auth/oidc.go | 143 ++++++++++++++++++++++++++++ cmd/config.go | 23 +++++ cmd/config_import.go | 2 + frontend/src/components/Sidebar.vue | 3 +- frontend/src/main.js | 4 +- frontend/src/utils/auth.js | 11 ++- go.mod | 6 ++ go.sum | 23 +++++ http/auth.go | 22 ++++- http/static.go | 17 +++- storage/bolt/auth.go | 2 + storage/bolt/importer/conf.go | 16 ++++ users/users.go | 1 + 13 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 auth/oidc.go diff --git a/auth/oidc.go b/auth/oidc.go new file mode 100644 index 00000000..9ba2558c --- /dev/null +++ b/auth/oidc.go @@ -0,0 +1,143 @@ +package auth + +import ( + "context" + "fmt" + "github.com/filebrowser/filebrowser/v2/settings" + "github.com/filebrowser/filebrowser/v2/users" + "log" + "math/rand" + "net/http" + "os" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +// MethodOIDCAuth is used to identify oidc auth. +const MethodOIDCAuth settings.AuthMethod = "oidc" + +// OIDCAuth is an Open ID Connect auther implementation. +type OIDCAuth struct { + OIDC *OAuthClient `json:"oidc" yaml:"oidc"` +} + +// Auth is requested by the identity provider in the callback phase of an oauth code flow. +func (a OIDCAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) { + cookie, _ := r.Cookie("auth") + if cookie != nil { + return nil, nil + } + + log.Println("oidc auth callback") + u, err := a.OIDC.HandleAuthCallback(r, usr, srv) + + return u, err +} + +// LoginPage tells that oidc auth doesn't require a login page. +func (a OIDCAuth) LoginPage() bool { + return false +} + +// OAuthClient describes the oidc connector parameters. +type OAuthClient struct { + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + Issuer string `json:"issuer"` + RedirectURL string `json:"redirectURL"` + OAuth2Config oauth2.Config `json:"-"` + Verifier *oidc.IDTokenVerifier `json:"-"` +} + +// InitClient configures the connector via oidc discovery. +func (o *OAuthClient) InitClient() { + ctx := context.Background() + provider, err := oidc.NewProvider(ctx, o.Issuer) + if err != nil { + fmt.Println(err) + log.Fatal(err) + } + + o.Verifier = provider.Verifier(&oidc.Config{ClientID: o.ClientID}) + o.OAuth2Config = oauth2.Config{ + ClientID: o.ClientID, + ClientSecret: o.ClientSecret, + RedirectURL: o.RedirectURL, + Endpoint: provider.Endpoint(), + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + } +} + +// InitAuthFlow triggers oidc authentication flow +func (o *OAuthClient) InitAuthFlow(w http.ResponseWriter, r *http.Request) { + o.InitClient() + state := fmt.Sprintf("%x", rand.Uint32()) + nonce := fmt.Sprintf("%x", rand.Uint32()) + url := o.OAuth2Config.AuthCodeURL(state, oidc.Nonce(nonce)) + + log.Println("oidc init flow ", url) + w.Header().Set("Set-Cookie", "state="+state+"; path=/") + http.Redirect(w, r, url, http.StatusMovedPermanently) +} + +// HandleAuthCallback manages code exchange and obtains the id token. +func (o *OAuthClient) HandleAuthCallback(r *http.Request, usr users.Store, srv *settings.Server) (*users.User, error) { + o.InitClient() + + code := r.URL.Query().Get("code") + stateQuery := r.URL.Query().Get("state") + stateCookie, _ := r.Cookie("state") + + if code == "" || stateQuery == "" || stateQuery != stateCookie.Value { + log.Fatal("Invalid request") + return nil, os.ErrInvalid + } + + // Exchange code for token + oauth2Token, err := o.OAuth2Config.Exchange(context.Background(), code) + if err != nil { + log.Fatal(err) + return nil, err + } + log.Println("oidc got token") + + // Parse id token + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + log.Fatal("Invalid token") + return nil, nil + } + log.Println("oidc parsed token") + + // Verify id token + idToken, err := o.Verifier.Verify(context.Background(), rawIDToken) + if err != nil { + log.Fatal("oidc verify failed") + return nil, err + } + log.Println("oidc verified token") + + // Extract claims + var claims struct { + Email string `json:"email"` + Verified bool `json:"email_verified"` + Username string `json:"preferred_username"` + Profile string `json:"profile"` + } + if err := idToken.Claims(&claims); err != nil { + log.Fatal(err) + return nil, err + } + + // Find filebrowser user by oidc username + u, err := usr.Get(srv.Root, claims.Username) + if err != nil { + log.Println("oidc authenticated but no matching filebrowser user") + return nil, os.ErrPermission + } + u.AuthSource = "oidc" + log.Println("oidc success (user, claims) ", u, claims) + + return u, nil +} diff --git a/cmd/config.go b/cmd/config.go index 4287f3c2..1f6288c7 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -41,6 +41,11 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("recaptcha.key", "", "ReCaptcha site key") flags.String("recaptcha.secret", "", "ReCaptcha secret") + flags.String("oidc.clientID", "", "Open ID Connect Client ID for auth.method=oidc") + flags.String("oidc.clientSecret", "", "Open ID Connect Client Secret for auth.method=oidc") + flags.String("oidc.issuer", "", "Open ID Connect Configuration Issuer auth.method=oidc") + flags.String("oidc.redirectURL", "", "Open ID Connect Redirect URL for auth.method=oidc") + flags.String("branding.name", "", "replace 'File Browser' by this name") flags.String("branding.color", "", "set the theme color") flags.String("branding.files", "", "path to directory with images and custom styles") @@ -116,6 +121,24 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings. auther = jsonAuth } + if method == auth.MethodOIDCAuth { + oidcAuth := &auth.OIDCAuth{} + id := mustGetString(flags, "oidc.clientID") + secret := mustGetString(flags, "oidc.clientSecret") + url := mustGetString(flags, "oidc.issuer") + redirect := mustGetString(flags, "oidc.redirectURL") + + if id != "" && secret != "" && url != "" && redirect != "" { + oidcAuth.OIDC = &auth.OAuthClient{ + ClientID: id, + ClientSecret: secret, + Issuer: url, + RedirectURL: redirect, + } + } + auther = oidcAuth + } + if method == auth.MethodHookAuth { command := mustGetString(flags, "auth.command") diff --git a/cmd/config_import.go b/cmd/config_import.go index b87eb4e3..8d6a7c0a 100644 --- a/cmd/config_import.go +++ b/cmd/config_import.go @@ -66,6 +66,8 @@ The path must be for a json or yaml file.`, switch file.Settings.AuthMethod { case auth.MethodJSONAuth: auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth) + case auth.MethodOIDCAuth: + auther = getAuther(auth.OIDCAuth{}, rawAuther).(*auth.OIDCAuth) case auth.MethodNoAuth: auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth) case auth.MethodProxyAuth: diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 4394f55a..c9c31508 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -121,6 +121,7 @@ import { disableUsedPercentage, noAuth, loginPage, + authMethod, } from "@/utils/constants"; import { files as api } from "@/api"; import ProgressBar from "vue-simple-progress"; @@ -141,7 +142,7 @@ export default { version: () => version, disableExternal: () => disableExternal, disableUsedPercentage: () => disableUsedPercentage, - canLogout: () => !noAuth && loginPage, + canLogout: () => (!noAuth && loginPage) || authMethod === "oidc" }, asyncComputed: { usage: { diff --git a/frontend/src/main.js b/frontend/src/main.js index 28ecf43f..e5d11a68 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -5,7 +5,7 @@ import store from "@/store"; import router from "@/router"; import i18n from "@/i18n"; import Vue from "@/utils/vue"; -import { recaptcha, loginPage } from "@/utils/constants"; +import { recaptcha, loginPage, authMethod } from "@/utils/constants"; import { login, validateLogin } from "@/utils/auth"; import App from "@/App"; @@ -15,7 +15,7 @@ sync(store, router); async function start() { try { - if (loginPage) { + if (loginPage || authMethod === "oidc") { await validateLogin(); } else { await login("", "", ""); diff --git a/frontend/src/utils/auth.js b/frontend/src/utils/auth.js index 31cb57c4..393cb23e 100644 --- a/frontend/src/utils/auth.js +++ b/frontend/src/utils/auth.js @@ -2,6 +2,7 @@ import store from "@/store"; import router from "@/router"; import { Base64 } from "js-base64"; import { baseURL } from "@/utils/constants"; +import cookie from "@/utils/cookie"; export function parseToken(token) { const parts = token.split("."); @@ -20,9 +21,15 @@ export function parseToken(token) { } export async function validateLogin() { + let jwt = localStorage.getItem("jwt") + + if (!jwt || jwt === "null") { + jwt = cookie("auth"); + } + try { - if (localStorage.getItem("jwt")) { - await renew(localStorage.getItem("jwt")); + if (jwt) { + await renew(jwt); } } catch (_) { console.warn('Invalid JWT token in storage') // eslint-disable-line diff --git a/go.mod b/go.mod index b50ba09c..b944dcbe 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/asdine/storm/v3 v3.2.1 + github.com/coreos/go-oidc/v3 v3.5.0 github.com/disintegration/imaging v1.6.2 github.com/dsoprea/go-exif/v3 v3.0.0-20201216222538-db167117f483 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 @@ -25,6 +26,7 @@ require ( go.etcd.io/bbolt v1.3.7 golang.org/x/crypto v0.6.0 golang.org/x/image v0.5.0 + golang.org/x/oauth2 v0.5.0 golang.org/x/text v0.7.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 @@ -38,8 +40,10 @@ require ( github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-errors/errors v1.1.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/geo v0.0.0-20200319012246-673a6f80352d // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect @@ -59,6 +63,8 @@ require ( github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/net v0.6.0 // indirect golang.org/x/sys v0.5.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3b5fde8a..9ef0352d 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -54,6 +55,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw= +github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -91,6 +94,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= @@ -123,7 +128,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -140,6 +147,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -237,6 +245,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -274,6 +283,7 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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= @@ -352,6 +362,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v 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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -363,6 +375,10 @@ 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.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= +golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= 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= @@ -414,11 +430,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 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= @@ -426,6 +444,7 @@ 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -572,7 +591,11 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/http/auth.go b/http/auth.go index 4df68262..a9e5edfb 100644 --- a/http/auth.go +++ b/http/auth.go @@ -112,6 +112,8 @@ var loginHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, e return http.StatusForbidden, nil } else if err != nil { return http.StatusInternalServerError, err + } else if user != nil && user.AuthSource == "oidc" { + return setTokenCookie(w, r, d, user) } else { return printToken(w, r, d, user) } @@ -176,7 +178,7 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data return printToken(w, r, d, d.user) }) -func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) { +func getToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (string, error) { claims := &authToken{ User: userInfo{ ID: user.ID, @@ -198,6 +200,12 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString(d.settings.Key) + + return signed, err +} + +func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.User) (int, error) { + signed, err := getToken(w, r, d, user) if err != nil { return http.StatusInternalServerError, err } @@ -208,3 +216,15 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use } return 0, nil } + +func setTokenCookie(w http.ResponseWriter, r *http.Request, d *data, user *users.User) (int, error) { + signed, err := getToken(w, r, d, user) + if err != nil { + return http.StatusInternalServerError, err + } + + w.Header().Set("Set-Cookie", "auth="+signed+"; path=/") + http.Redirect(w, r, "/files", http.StatusMovedPermanently) + + return 0, nil +} diff --git a/http/static.go b/http/static.go index f438d76c..fd778e82 100644 --- a/http/static.go +++ b/http/static.go @@ -18,7 +18,7 @@ import ( "github.com/filebrowser/filebrowser/v2/version" ) -func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys fs.FS, file, contentType string) (int, error) { +func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *data, fSys fs.FS, file, contentType string) (int, error) { w.Header().Set("Content-Type", contentType) auther, err := d.store.Auth.Get(d.settings.AuthMethod) @@ -74,6 +74,21 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys } } + if d.settings.AuthMethod == auth.MethodOIDCAuth { + raw, err := d.store.Auth.Get(d.settings.AuthMethod) //nolint:govet + if err != nil { + return http.StatusInternalServerError, err + } + + auther := raw.(*auth.OIDCAuth) + cookie, _ := r.Cookie("auth") + + if cookie == nil { + auther.OIDC.InitAuthFlow(w, r) + return 0, nil + } + } + b, err := json.Marshal(data) if err != nil { return http.StatusInternalServerError, err diff --git a/storage/bolt/auth.go b/storage/bolt/auth.go index cf15a8fe..5eeceec3 100644 --- a/storage/bolt/auth.go +++ b/storage/bolt/auth.go @@ -18,6 +18,8 @@ func (s authBackend) Get(t settings.AuthMethod) (auth.Auther, error) { switch t { case auth.MethodJSONAuth: auther = &auth.JSONAuth{} + case auth.MethodOIDCAuth: + auther = &auth.OIDCAuth{} case auth.MethodProxyAuth: auther = &auth.ProxyAuth{} case auth.MethodHookAuth: diff --git a/storage/bolt/importer/conf.go b/storage/bolt/importer/conf.go index bafeb452..d63b296f 100644 --- a/storage/bolt/importer/conf.go +++ b/storage/bolt/importer/conf.go @@ -44,6 +44,12 @@ type oldConf struct { Secret string `json:"secret" yaml:"secret" toml:"secret"` Host string `json:"host" yaml:"host" toml:"host"` } `json:"recaptcha" yaml:"recaptcha" toml:"recaptcha"` + OIDC struct { + ClientID string `json:"clientID" yaml:"clientID" toml:"clientID"` + ClientSecret string `json:"clientSecret" yaml:"clientSecret" toml:"clientSecret"` + Issuer string `json:"issuer" yaml:"issuer" toml:"issuer"` + RedirectURL string `json:"redirectURL" yaml:"redirectURL" toml:"redirectURL"` + } `json:"oidc" yaml:"oidc" toml:"oidc"` Auth oldAuth `json:"auth" yaml:"auth" toml:"auth"` } @@ -150,6 +156,16 @@ func importConf(db *storm.DB, path string, sto *storage.Storage) error { case "proxy": auther = &auth.ProxyAuth{Header: cfg.Auth.Header} s.AuthMethod = auth.MethodProxyAuth + case "oidc": + auther = &auth.OIDCAuth{ + OIDC: &auth.OAuthClient{ + ClientID: cfg.OIDC.ClientID, + ClientSecret: cfg.OIDC.ClientSecret, + Issuer: cfg.OIDC.Issuer, + RedirectURL: cfg.OIDC.RedirectURL, + }, + } + s.AuthMethod = auth.MethodOIDCAuth case "hook": auther = &auth.HookAuth{Command: cfg.Auth.Command} s.AuthMethod = auth.MethodHookAuth diff --git a/users/users.go b/users/users.go index ec613856..26dcbeb1 100644 --- a/users/users.go +++ b/users/users.go @@ -36,6 +36,7 @@ type User struct { Rules []rules.Rule `json:"rules"` HideDotfiles bool `json:"hideDotfiles"` DateFormat bool `json:"dateFormat"` + AuthSource string `json:"authSource"` } // GetRules implements rules.Provider.