Get groups from directory api
This commit is contained in:
parent
36370f8f2a
commit
3f55e2da72
1 changed files with 83 additions and 15 deletions
|
@ -5,14 +5,17 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc"
|
"github.com/coreos/go-oidc"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector"
|
"github.com/dexidp/dex/connector"
|
||||||
|
"github.com/dexidp/dex/pkg/log"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
|
"google.golang.org/api/admin/directory/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -30,11 +33,21 @@ type Config struct {
|
||||||
// Optional list of whitelisted domains
|
// Optional list of whitelisted domains
|
||||||
// If this field is nonempty, only users from a listed domain will be allowed to log in
|
// If this field is nonempty, only users from a listed domain will be allowed to log in
|
||||||
HostedDomains []string `json:"hostedDomains"`
|
HostedDomains []string `json:"hostedDomains"`
|
||||||
|
|
||||||
|
// Optional path to service account json
|
||||||
|
// If nonempty, and groups claim is made, will use authentication from file to
|
||||||
|
// check groups with the admin directory api
|
||||||
|
ServiceAccountFilePath string `json:"serviceAccountFilePath"`
|
||||||
|
|
||||||
|
// Required if ServiceAccountFilePath
|
||||||
|
// The email of a GSuite super user which the service account will impersonate
|
||||||
|
// when listing groups
|
||||||
|
AdminEmail string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open returns a connector which can be used to login users through an upstream
|
// Open returns a connector which can be used to login users through an upstream
|
||||||
// OpenID Connect provider.
|
// OpenID Connect provider.
|
||||||
func (c *Config) Open(id string, logger logrus.FieldLogger) (conn connector.Connector, err error) {
|
func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
provider, err := oidc.NewProvider(ctx, issuerURL)
|
provider, err := oidc.NewProvider(ctx, issuerURL)
|
||||||
|
@ -66,6 +79,8 @@ func (c *Config) Open(id string, logger logrus.FieldLogger) (conn connector.Conn
|
||||||
logger: logger,
|
logger: logger,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
hostedDomains: c.HostedDomains,
|
hostedDomains: c.HostedDomains,
|
||||||
|
serviceAccountFilePath: c.ServiceAccountFilePath,
|
||||||
|
adminEmail: c.AdminEmail,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +95,10 @@ type googleConnector struct {
|
||||||
verifier *oidc.IDTokenVerifier
|
verifier *oidc.IDTokenVerifier
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
logger logrus.FieldLogger
|
logger log.Logger
|
||||||
hostedDomains []string
|
hostedDomains []string
|
||||||
|
serviceAccountFilePath string
|
||||||
|
adminEmail string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *googleConnector) Close() error {
|
func (c *googleConnector) Close() error {
|
||||||
|
@ -131,7 +148,7 @@ func (c *googleConnector) HandleCallback(s connector.Scopes, r *http.Request) (i
|
||||||
return identity, fmt.Errorf("google: failed to get token: %v", err)
|
return identity, fmt.Errorf("google: failed to get token: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.createIdentity(r.Context(), identity, token)
|
return c.createIdentity(r.Context(), identity, s, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh is implemented for backwards compatibility, even though it's a no-op.
|
// Refresh is implemented for backwards compatibility, even though it's a no-op.
|
||||||
|
@ -145,10 +162,10 @@ func (c *googleConnector) Refresh(ctx context.Context, s connector.Scopes, ident
|
||||||
return identity, fmt.Errorf("google: failed to get token: %v", err)
|
return identity, fmt.Errorf("google: failed to get token: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.createIdentity(ctx, identity, token)
|
return c.createIdentity(ctx, identity, s, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *googleConnector) createIdentity(ctx context.Context, identity connector.Identity, token *oauth2.Token) (connector.Identity, error) {
|
func (c *googleConnector) createIdentity(ctx context.Context, identity connector.Identity, s connector.Scopes, token *oauth2.Token) (connector.Identity, error) {
|
||||||
rawIDToken, ok := token.Extra("id_token").(string)
|
rawIDToken, ok := token.Extra("id_token").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return identity, errors.New("google: no id_token in token response")
|
return identity, errors.New("google: no id_token in token response")
|
||||||
|
@ -182,12 +199,63 @@ func (c *googleConnector) createIdentity(ctx context.Context, identity connector
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var groups []string
|
||||||
|
if s.Groups {
|
||||||
|
groups, err = c.getGroups(claims.Email)
|
||||||
|
if err != nil {
|
||||||
|
return identity, fmt.Errorf("google: could not retrieve groups: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
identity = connector.Identity{
|
identity = connector.Identity{
|
||||||
UserID: idToken.Subject,
|
UserID: idToken.Subject,
|
||||||
Username: claims.Username,
|
Username: claims.Username,
|
||||||
Email: claims.Email,
|
Email: claims.Email,
|
||||||
EmailVerified: claims.EmailVerified,
|
EmailVerified: claims.EmailVerified,
|
||||||
ConnectorData: []byte(token.RefreshToken),
|
ConnectorData: []byte(token.RefreshToken),
|
||||||
|
Groups: groups,
|
||||||
}
|
}
|
||||||
return identity, nil
|
return identity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *googleConnector) getGroups(email string) ([]string, error) {
|
||||||
|
srv, err := createDirectoryService(c.serviceAccountFilePath, c.adminEmail)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create directory service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupsList, err := srv.Groups.List().UserKey(email).Do()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not list groups: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var userGroups []string
|
||||||
|
for _, group := range groupsList.Groups {
|
||||||
|
userGroups = append(userGroups, group.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDirectoryService(serviceAccountFilePath string, email string) (*admin.Service, error) {
|
||||||
|
jsonCredentials, err := ioutil.ReadFile(serviceAccountFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading credentials from file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupReadonlyScope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse client secret file to config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Subject = email
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
client := config.Client(ctx)
|
||||||
|
|
||||||
|
srv, err := admin.New(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create directory service %v", err)
|
||||||
|
}
|
||||||
|
return srv, nil
|
||||||
|
}
|
||||||
|
|
Reference in a new issue