diff --git a/go.mod b/go.mod index 47b27f35e..f29a96936 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/buckket/go-blurhash v1.1.0 + github.com/coreos/go-oidc/v3 v3.0.0 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/dsoprea/go-exif v0.0.0-20210512055020-8213cfabc61b // indirect github.com/dsoprea/go-exif/v2 v2.0.0-20210512055020-8213cfabc61b // indirect @@ -50,6 +51,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect github.com/wagslane/go-password-validator v0.3.0 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect golang.org/x/text v0.3.6 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 08ee5d58e..2bf1667ce 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= @@ -14,6 +15,8 @@ github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZ github.com/buckket/go-blurhash v1.1.0/go.mod h1:aT2iqo5W9vu9GpyoLErKfTHwgODsZp3bQfXjXJUxNb8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ= +github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -352,6 +355,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -362,6 +366,7 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -374,8 +379,11 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 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= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -424,6 +432,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= @@ -452,6 +461,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/api/client/auth/auth.go b/internal/api/client/auth/auth.go index 7cddc3e74..caa70aa63 100644 --- a/internal/api/client/auth/auth.go +++ b/internal/api/client/auth/auth.go @@ -26,6 +26,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/router" ) @@ -36,6 +37,8 @@ const ( OauthTokenPath = "/oauth/token" // OauthAuthorizePath is the API path for authorization requests (eg., authorize this app to act on my behalf as a user) OauthAuthorizePath = "/oauth/authorize" + // CallbackPath is the API path for receiving callback tokens from external OIDC providers + CallbackPath = "/auth/callback" sessionUserID = "userid" sessionClientID = "client_id" @@ -61,15 +64,17 @@ type Module struct { config *config.Config db db.DB server oauth.Server + idp oidc.IDP log *logrus.Logger } // New returns a new auth module -func New(config *config.Config, db db.DB, server oauth.Server, log *logrus.Logger) api.ClientModule { +func New(config *config.Config, db db.DB, server oauth.Server, idp oidc.IDP, log *logrus.Logger) api.ClientModule { return &Module{ config: config, db: db, server: server, + idp: idp, log: log, } } diff --git a/internal/api/client/auth/callback.go b/internal/api/client/auth/callback.go new file mode 100644 index 000000000..99d89bd0b --- /dev/null +++ b/internal/api/client/auth/callback.go @@ -0,0 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package auth + + diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go index 316be614e..7619c1de2 100644 --- a/internal/cliactions/server/server.go +++ b/internal/cliactions/server/server.go @@ -42,6 +42,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/router" timelineprocessing "github.com/superseriousbusiness/gotosocial/internal/timeline" @@ -121,8 +122,13 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log return fmt.Errorf("error starting processor: %s", err) } + idp, err := oidc.NewIDP(c) + if err != nil { + return fmt.Errorf("error creating oidc idp: %s", err) + } + // build client api modules - authModule := auth.New(c, dbService, oauthServer, log) + authModule := auth.New(c, dbService, oauthServer, idp, log) accountModule := account.New(c, processor, log) instanceModule := instance.New(c, processor, log) appsModule := app.New(c, processor, log) diff --git a/internal/cliactions/testrig/testrig.go b/internal/cliactions/testrig/testrig.go index c669dd851..f7a52a694 100644 --- a/internal/cliactions/testrig/testrig.go +++ b/internal/cliactions/testrig/testrig.go @@ -37,6 +37,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/cliactions" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gotosocial" + "github.com/superseriousbusiness/gotosocial/internal/oidc" "github.com/superseriousbusiness/gotosocial/internal/web" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -66,8 +67,13 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log return fmt.Errorf("error starting processor: %s", err) } + idp, err := oidc.NewIDP(c) + if err != nil { + return fmt.Errorf("error creating oidc idp: %s", err) + } + // build client api modules - authModule := auth.New(c, dbService, oauthServer, log) + authModule := auth.New(c, dbService, oauthServer, idp, log) accountModule := account.New(c, processor, log) instanceModule := instance.New(c, processor, log) appsModule := app.New(c, processor, log) diff --git a/internal/config/default.go b/internal/config/default.go index 099eead1c..27b6d3a45 100644 --- a/internal/config/default.go +++ b/internal/config/default.go @@ -1,5 +1,7 @@ package config +import "github.com/coreos/go-oidc/v3/oidc" + // TestDefault returns a default config for testing func TestDefault() *Config { defaults := GetTestDefaults() @@ -52,6 +54,16 @@ func TestDefault() *Config { CertDir: defaults.LetsEncryptCertDir, EmailAddress: defaults.LetsEncryptEmailAddress, }, + OIDCConfig: &OIDCConfig{ + Enabled: defaults.OIDCEnabled, + IDPID: defaults.OIDCIdpID, + IDPName: defaults.OIDCIdpName, + SkipVerification: defaults.OIDCSkipVerification, + Issuer: defaults.OIDCIssuer, + ClientID: defaults.OIDCClientID, + ClientSecret: defaults.OIDCClientSecret, + Scopes: defaults.OIDCScopes, + }, } } @@ -107,6 +119,16 @@ func Default() *Config { CertDir: defaults.LetsEncryptCertDir, EmailAddress: defaults.LetsEncryptEmailAddress, }, + OIDCConfig: &OIDCConfig{ + Enabled: defaults.OIDCEnabled, + IDPID: defaults.OIDCIdpID, + IDPName: defaults.OIDCIdpName, + SkipVerification: defaults.OIDCSkipVerification, + Issuer: defaults.OIDCIssuer, + ClientID: defaults.OIDCClientID, + ClientSecret: defaults.OIDCClientSecret, + Scopes: defaults.OIDCScopes, + }, } } @@ -157,6 +179,15 @@ func GetDefaults() Defaults { LetsEncryptEnabled: true, LetsEncryptCertDir: "/gotosocial/storage/certs", LetsEncryptEmailAddress: "", + + OIDCEnabled: false, + OIDCIdpID: "", + OIDCIdpName: "", + OIDCSkipVerification: false, + OIDCIssuer: "", + OIDCClientID: "", + OIDCClientSecret: "", + OIDCScopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"}, } } diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go new file mode 100644 index 000000000..e8ea7221e --- /dev/null +++ b/internal/oidc/oidc.go @@ -0,0 +1,76 @@ +package oidc + +import ( + "context" + "fmt" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/superseriousbusiness/gotosocial/internal/config" + "golang.org/x/oauth2" +) + +const ( + // CallbackPath is the API path for receiving callback tokens from external OIDC providers + CallbackPath = "/auth/callback" + profileScope = "profile" + emailScope = "email" + groupsScope = "groups" +) + +type IDP interface { +} + +type idp struct { + oauth2Config oauth2.Config + provider *oidc.Provider + idTokenVerifier *oidc.IDTokenVerifier +} + +func NewIDP(config *config.Config) (IDP, error) { + + // oidc isn't enabled so we don't need to do anything + if !config.OIDCConfig.Enabled { + return nil, nil + } + + // validate config fields + if config.OIDCConfig.IDPID == "" { + return nil, fmt.Errorf("not set: IDPID") + } + + if config.OIDCConfig.IDPName == "" { + return nil, fmt.Errorf("not set: IDPName") + } + + aaaaaaaaaaaaaaaaaaaaaaaaaaaa + + + provider, err := oidc.NewProvider(context.Background(), config.OIDCConfig.Issuer) + if err != nil { + return nil, err + } + + oauth2Config := oauth2.Config{ + // client_id and client_secret of the client. + ClientID: config.OIDCConfig.ClientID, + ClientSecret: config.OIDCConfig.ClientSecret, + + // The redirectURL. + RedirectURL: fmt.Sprintf("%s://%s%s", config.Protocol, config.Host, CallbackPath), + + // Discovery returns the OAuth2 endpoints. + Endpoint: provider.Endpoint(), + + // "openid" is a required scope for OpenID Connect flows. + // + // Other scopes, such as "groups" can be requested. + Scopes: config.OIDCConfig.Scopes, + } + + idTokenVerifier := provider.Verifier(&oidc.Config{ClientID: config.OIDCConfig.ClientID}) + + return &idp{ + oauth2Config: oauth2Config, + idTokenVerifier: idTokenVerifier, + }, nil +}