From 8e0d32d3e19e1d64d444679a9f6b8ba98a89a93e Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Wed, 21 Jul 2021 19:00:45 +0200 Subject: [PATCH] bit more progress --- internal/api/client/auth/auth.go | 7 +- internal/api/client/auth/callback.go | 18 ++++ internal/api/client/auth/signin.go | 8 +- internal/cliactions/server/server.go | 2 +- internal/cliactions/testrig/testrig.go | 2 +- internal/oidc/claims.go | 25 ++++++ internal/oidc/handlecallback.go | 68 +++++++++++++++ internal/oidc/idp.go | 115 +++++++++++++++++++++++++ internal/oidc/oidc.go | 76 ---------------- 9 files changed, 241 insertions(+), 80 deletions(-) create mode 100644 internal/oidc/claims.go create mode 100644 internal/oidc/handlecallback.go create mode 100644 internal/oidc/idp.go delete mode 100644 internal/oidc/oidc.go diff --git a/internal/api/client/auth/auth.go b/internal/api/client/auth/auth.go index caa70aa63..83f2cee1e 100644 --- a/internal/api/client/auth/auth.go +++ b/internal/api/client/auth/auth.go @@ -38,7 +38,10 @@ const ( // 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" + CallbackPath = oidc.CallbackPath + + callbackStateParam = "state" + callbackCodeParam = "code" sessionUserID = "userid" sessionClientID = "client_id" @@ -89,6 +92,8 @@ func (m *Module) Route(s router.Router) error { s.AttachHandler(http.MethodGet, OauthAuthorizePath, m.AuthorizeGETHandler) s.AttachHandler(http.MethodPost, OauthAuthorizePath, m.AuthorizePOSTHandler) + s.AttachHandler(http.MethodGet, CallbackPath, m.CallbackGETHandler) + s.AttachMiddleware(m.OauthTokenMiddleware) return nil } diff --git a/internal/api/client/auth/callback.go b/internal/api/client/auth/callback.go index 99d89bd0b..a3f56a77a 100644 --- a/internal/api/client/auth/callback.go +++ b/internal/api/client/auth/callback.go @@ -18,4 +18,22 @@ package auth +import ( + "net/http" + "github.com/gin-gonic/gin" +) + +// CallbackGETHandler parses a token from an external auth provider. +func (m *Module) CallbackGETHandler(c *gin.Context) { + state := c.Query(callbackStateParam) + code := c.Query(callbackCodeParam) + + claims, err := m.idp.HandleCallback(c.Request.Context(), state, code) + if err != nil { + c.String(http.StatusForbidden, err.Error()) + return + } + + c.JSON(http.StatusOK, claims) +} diff --git a/internal/api/client/auth/signin.go b/internal/api/client/auth/signin.go index 7974a8cfa..94b8fd2a2 100644 --- a/internal/api/client/auth/signin.go +++ b/internal/api/client/auth/signin.go @@ -39,7 +39,13 @@ type login struct { // The idea is to present a sign in page to the user, where they can enter their username and password. // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler func (m *Module) SignInGETHandler(c *gin.Context) { - m.log.WithField("func", "SignInGETHandler").Trace("serving sign in html") + l := m.log.WithField("func", "SignInGETHandler") + l.Trace("entering sign in handler") + if m.idp != nil && m.config.OIDCConfig.Issuer != "" { + l.Debug("redirecting to external idp at %s", m.config.OIDCConfig.Issuer) + c.Redirect(http.StatusFound, m.config.OIDCConfig.Issuer) + return + } c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{}) } diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go index 7619c1de2..3c4f97dea 100644 --- a/internal/cliactions/server/server.go +++ b/internal/cliactions/server/server.go @@ -122,7 +122,7 @@ 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) + idp, err := oidc.NewIDP(c, log) if err != nil { return fmt.Errorf("error creating oidc idp: %s", err) } diff --git a/internal/cliactions/testrig/testrig.go b/internal/cliactions/testrig/testrig.go index f7a52a694..e2b97fe61 100644 --- a/internal/cliactions/testrig/testrig.go +++ b/internal/cliactions/testrig/testrig.go @@ -67,7 +67,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log return fmt.Errorf("error starting processor: %s", err) } - idp, err := oidc.NewIDP(c) + idp, err := oidc.NewIDP(c, log) if err != nil { return fmt.Errorf("error creating oidc idp: %s", err) } diff --git a/internal/oidc/claims.go b/internal/oidc/claims.go new file mode 100644 index 000000000..28ff21391 --- /dev/null +++ b/internal/oidc/claims.go @@ -0,0 +1,25 @@ +/* + 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 oidc + +// Claims represents claims as found in an id_token returned from an OIDC flow. +type Claims struct { + Email string `json:"email"` + Groups []string `json:"groups"` +} diff --git a/internal/oidc/handlecallback.go b/internal/oidc/handlecallback.go new file mode 100644 index 000000000..45db201fd --- /dev/null +++ b/internal/oidc/handlecallback.go @@ -0,0 +1,68 @@ +/* + 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 oidc + +import ( + "context" + "errors" + "fmt" +) + +func (i *idp) HandleCallback(ctx context.Context, state string, code string) (*Claims, error) { + l := i.log.WithField("func", "HandleCallback") + + if state == "" { + return nil, errors.New("state was empty string") + } + + if code == "" { + return nil, errors.New("code was empty string") + } + + // TODO: verify state + + l.Debug("exchanging code for oauth2token") + oauth2Token, err := i.oauth2Config.Exchange(ctx, code) + if err != nil { + return nil, fmt.Errorf("error exchanging code for oauth2token: %s", err) + } + + l.Debug("extracting id_token") + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + return nil, errors.New("no id_token in oauth2token") + } + l.Debug("raw id token: %s", rawIDToken) + + // Parse and verify ID Token payload. + l.Debug("verifying id_token") + idTokenVerifier := i.provider.Verifier(i.oidcConf) + idToken, err := idTokenVerifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, fmt.Errorf("could not verify id token: %s", err) + } + + l.Debug("extracting claims from id_token") + claims := &Claims{} + if err := idToken.Claims(claims); err != nil { + return nil, fmt.Errorf("could not parse claims from idToken: %s", err) + } + + return claims, nil +} diff --git a/internal/oidc/idp.go b/internal/oidc/idp.go new file mode 100644 index 000000000..a9b3e53fb --- /dev/null +++ b/internal/oidc/idp.go @@ -0,0 +1,115 @@ +/* + 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 oidc + +import ( + "context" + "fmt" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/sirupsen/logrus" + "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 { + HandleCallback(ctx context.Context, state string, code string) (*Claims, error) +} + +type idp struct { + oauth2Config oauth2.Config + provider *oidc.Provider + oidcConf *oidc.Config + log *logrus.Logger +} + +func NewIDP(config *config.Config, log *logrus.Logger) (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") + } + if config.OIDCConfig.Issuer == "" { + return nil, fmt.Errorf("not set: Issuer") + } + if config.OIDCConfig.ClientID == "" { + return nil, fmt.Errorf("not set: ClientID") + } + if config.OIDCConfig.ClientSecret == "" { + return nil, fmt.Errorf("not set: ClientSecret") + } + if len(config.OIDCConfig.Scopes) == 0 { + return nil, fmt.Errorf("not set: Scopes") + } + + 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, + } + + // create a config for verifier creation + oidcConf := &oidc.Config{ + ClientID: config.OIDCConfig.ClientID, + } + if config.OIDCConfig.SkipVerification { + oidcConf.SkipClientIDCheck = true + oidcConf.SkipExpiryCheck = true + oidcConf.SkipIssuerCheck = true + } + + return &idp{ + oauth2Config: oauth2Config, + oidcConf: oidcConf, + provider: provider, + log: log, + }, nil +} diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go deleted file mode 100644 index e8ea7221e..000000000 --- a/internal/oidc/oidc.go +++ /dev/null @@ -1,76 +0,0 @@ -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 -}