mirror of
https://github.com/Luzifer/nginx-sso.git
synced 2024-10-18 23:54:20 +00:00
Knut Ahlers
93d242c404
which then causes logins with duration of more than 60m to time out and not be able to refresh as of the missing refresh token. The "offline" access type should ensure the token always contains a refresh token and the user can be active for longer than 60m. Signed-off-by: Knut Ahlers <knut@ahlers.me>
241 lines
7.1 KiB
Go
241 lines
7.1 KiB
Go
package google
|
|
|
|
import (
|
|
"context"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/google"
|
|
v2 "google.golang.org/api/oauth2/v2"
|
|
"google.golang.org/api/option"
|
|
yaml "gopkg.in/yaml.v2"
|
|
|
|
"github.com/gorilla/sessions"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/Luzifer/nginx-sso/plugins"
|
|
)
|
|
|
|
const (
|
|
userIDMethodFullEmail = "full-email"
|
|
userIDMethodLocalPart = "local-part"
|
|
userIDMethodUserID = "user-id"
|
|
)
|
|
|
|
type AuthGoogleOAuth struct {
|
|
ClientID string `yaml:"client_id"`
|
|
ClientSecret string `yaml:"client_secret"`
|
|
RedirectURL string `yaml:"redirect_url"`
|
|
|
|
RequireDomain string `yaml:"require_domain"`
|
|
UserIDMethod string `yaml:"user_id_method"`
|
|
|
|
cookie plugins.CookieConfig
|
|
cookieStore *sessions.CookieStore
|
|
}
|
|
|
|
func init() {
|
|
gob.Register(&oauth2.Token{})
|
|
}
|
|
|
|
func New(cs *sessions.CookieStore) *AuthGoogleOAuth {
|
|
return &AuthGoogleOAuth{
|
|
UserIDMethod: userIDMethodUserID,
|
|
cookieStore: cs,
|
|
}
|
|
}
|
|
|
|
// AuthenticatorID needs to return an unique string to identify
|
|
// this special authenticator
|
|
func (a *AuthGoogleOAuth) AuthenticatorID() (id string) { return "google_oauth" }
|
|
|
|
// Configure loads the configuration for the Authenticator from the
|
|
// global config.yaml file which is passed as a byte-slice.
|
|
// If no configuration for the Authenticator is supplied the function
|
|
// needs to return the ErrProviderUnconfigured
|
|
func (a *AuthGoogleOAuth) Configure(yamlSource []byte) (err error) {
|
|
envelope := struct {
|
|
Cookie plugins.CookieConfig `yaml:"cookie"`
|
|
Providers struct {
|
|
GoogleOAuth *AuthGoogleOAuth `yaml:"google_oauth"`
|
|
} `yaml:"providers"`
|
|
}{}
|
|
|
|
if err := yaml.Unmarshal(yamlSource, &envelope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if envelope.Providers.GoogleOAuth == nil {
|
|
return plugins.ErrProviderUnconfigured
|
|
}
|
|
|
|
a.ClientID = envelope.Providers.GoogleOAuth.ClientID
|
|
a.ClientSecret = envelope.Providers.GoogleOAuth.ClientSecret
|
|
a.RedirectURL = envelope.Providers.GoogleOAuth.RedirectURL
|
|
a.RequireDomain = envelope.Providers.GoogleOAuth.RequireDomain
|
|
|
|
if envelope.Providers.GoogleOAuth.UserIDMethod != "" {
|
|
a.UserIDMethod = envelope.Providers.GoogleOAuth.UserIDMethod
|
|
}
|
|
|
|
a.cookie = envelope.Cookie
|
|
|
|
return nil
|
|
}
|
|
|
|
// DetectUser is used to detect a user without a login form from
|
|
// a cookie, header or other methods
|
|
// If no user was detected the ErrNoValidUserFound needs to be
|
|
// returned
|
|
func (a *AuthGoogleOAuth) DetectUser(res http.ResponseWriter, r *http.Request) (user string, groups []string, err error) {
|
|
sess, err := a.cookieStore.Get(r, strings.Join([]string{a.cookie.Prefix, a.AuthenticatorID()}, "-"))
|
|
if err != nil {
|
|
return "", nil, plugins.ErrNoValidUserFound
|
|
}
|
|
|
|
token, ok := sess.Values["google_token"].(*oauth2.Token)
|
|
if !ok {
|
|
return "", nil, plugins.ErrNoValidUserFound
|
|
}
|
|
|
|
u, err := a.getUserFromToken(r.Context(), token)
|
|
if err != nil {
|
|
if err == plugins.ErrNoValidUserFound {
|
|
return "", nil, err
|
|
}
|
|
return "", nil, errors.Wrap(err, "Unable to fetch user info")
|
|
}
|
|
|
|
// We had a cookie, lets renew it
|
|
sess.Options = a.cookie.GetSessionOpts()
|
|
if err := sess.Save(r, res); err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
return u, nil, nil // TODO: Maybe get group info?
|
|
}
|
|
|
|
// Login is called when the user submits the login form and needs
|
|
// to authenticate the user or throw an error. If the user has
|
|
// successfully logged in the persistent cookie should be written
|
|
// in order to use DetectUser for the next login.
|
|
// With the login result an array of mfaConfig must be returned. In
|
|
// case there is no MFA config or the provider does not support MFA
|
|
// return nil.
|
|
// If the user did not login correctly the ErrNoValidUserFound
|
|
// needs to be returned
|
|
func (a *AuthGoogleOAuth) Login(res http.ResponseWriter, r *http.Request) (user string, mfaConfigs []plugins.MFAConfig, err error) {
|
|
var (
|
|
code = r.URL.Query().Get("code")
|
|
state = r.URL.Query().Get("state")
|
|
u string
|
|
)
|
|
|
|
if code == "" || state != a.AuthenticatorID() {
|
|
return "", nil, plugins.ErrNoValidUserFound
|
|
}
|
|
|
|
token, err := a.getOAuthConfig().Exchange(r.Context(), code)
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(err, "Unable to exchange token")
|
|
}
|
|
|
|
u, err = a.getUserFromToken(r.Context(), token)
|
|
if err != nil {
|
|
if err == plugins.ErrNoValidUserFound {
|
|
return "", nil, err
|
|
}
|
|
return "", nil, errors.Wrap(err, "Unable to fetch user info")
|
|
}
|
|
|
|
sess, _ := a.cookieStore.Get(r, strings.Join([]string{a.cookie.Prefix, a.AuthenticatorID()}, "-")) // #nosec G104 - On error empty session is returned
|
|
sess.Options = a.cookie.GetSessionOpts()
|
|
sess.Values["google_token"] = token
|
|
|
|
return u, nil, sess.Save(r, res)
|
|
}
|
|
|
|
// LoginFields needs to return the fields required for this login
|
|
// method. If no login using this method is possible the function
|
|
// needs to return nil.
|
|
func (a *AuthGoogleOAuth) LoginFields() (fields []plugins.LoginField) {
|
|
loginURL := a.getOAuthConfig().AuthCodeURL(a.AuthenticatorID(), oauth2.AccessTypeOffline)
|
|
|
|
return []plugins.LoginField{
|
|
{
|
|
Action: fmt.Sprintf("window.location.href='%s'", loginURL),
|
|
Label: "Trigger Login",
|
|
Name: "button",
|
|
Placeholder: "Sign in with Google",
|
|
Type: "button",
|
|
},
|
|
}
|
|
}
|
|
|
|
// Logout is called when the user visits the logout endpoint and
|
|
// needs to destroy any persistent stored cookies
|
|
func (a *AuthGoogleOAuth) Logout(res http.ResponseWriter, r *http.Request) (err error) {
|
|
sess, _ := a.cookieStore.Get(r, strings.Join([]string{a.cookie.Prefix, a.AuthenticatorID()}, "-")) // #nosec G104 - On error empty session is returned
|
|
sess.Options = a.cookie.GetSessionOpts()
|
|
sess.Options.MaxAge = -1 // Instant delete
|
|
return sess.Save(r, res)
|
|
}
|
|
|
|
// SupportsMFA returns the MFA detection capabilities of the login
|
|
// provider. If the provider can provide mfaConfig objects from its
|
|
// configuration return true. If this is true the login interface
|
|
// will display an additional field for this provider for the user
|
|
// to fill in their MFA token.
|
|
func (a *AuthGoogleOAuth) SupportsMFA() bool { return false }
|
|
|
|
func (a *AuthGoogleOAuth) getOAuthConfig() *oauth2.Config {
|
|
return &oauth2.Config{
|
|
ClientID: a.ClientID,
|
|
ClientSecret: a.ClientSecret,
|
|
Endpoint: google.Endpoint,
|
|
RedirectURL: a.RedirectURL,
|
|
Scopes: []string{
|
|
v2.UserinfoEmailScope,
|
|
v2.UserinfoProfileScope,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (a *AuthGoogleOAuth) getUserFromToken(ctx context.Context, token *oauth2.Token) (string, error) {
|
|
conf := a.getOAuthConfig()
|
|
|
|
httpClient := conf.Client(ctx, token)
|
|
client, err := v2.NewService(ctx, option.WithHTTPClient(httpClient))
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "Unable to instanciate OAuth2 API service")
|
|
}
|
|
|
|
tok, err := client.Tokeninfo().Context(ctx).Do()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "Unable to fetch token-info")
|
|
}
|
|
|
|
if a.RequireDomain != "" && !strings.HasSuffix(tok.Email, "@"+a.RequireDomain) {
|
|
// E-Mail domain is enforced, ignore all other users
|
|
return "", plugins.ErrNoValidUserFound
|
|
}
|
|
|
|
switch a.UserIDMethod {
|
|
case userIDMethodFullEmail:
|
|
return tok.Email, nil
|
|
|
|
case userIDMethodLocalPart:
|
|
return strings.Split(tok.Email, "@")[0], nil
|
|
|
|
case "":
|
|
fallthrough
|
|
case userIDMethodUserID:
|
|
return tok.UserId, nil
|
|
|
|
default:
|
|
return "", errors.Errorf("Invalid user_id_method %q", a.UserIDMethod)
|
|
}
|
|
}
|