Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix support for GSuite logins #3122

Merged
merged 1 commit into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ const (
GSuiteGroupsEndpoint = "https://www.googleapis.com/admin/directory/v1/groups"
// GSuiteGroupsScope is a scope to get access to admin groups API
GSuiteGroupsScope = "https://www.googleapis.com/auth/admin.directory.group.readonly"
// GSuiteDomainClaim is the domain name claim for GSuite
GSuiteDomainClaim = "hd"
)

// SCP is Secure Copy.
Expand Down
89 changes: 63 additions & 26 deletions lib/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ import (
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"

phttp "github.com/coreos/go-oidc/http"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
"github.com/gravitational/trace"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
)

func (s *AuthServer) getOrCreateOIDCClient(conn services.OIDCConnector) (*oidc.Client, error) {
Expand Down Expand Up @@ -261,7 +262,7 @@ func (a *AuthServer) validateOIDCAuthCallback(q url.Values) (*oidcAuthResponse,
}

// extract claims from both the id token and the userinfo endpoint and merge them
claims, err := a.getClaims(oidcClient, connector.GetIssuerURL(), connector.GetScope(), code)
claims, err := a.getClaims(oidcClient, connector, code)
if err != nil {
return nil, trace.WrapWithMessage(
// preserve the original error message, to avoid leaking
Expand Down Expand Up @@ -580,45 +581,42 @@ func claimsFromUserInfo(oidcClient *oidc.Client, issuerURL string, accessToken s
return claims, nil
}

func (a *AuthServer) claimsFromGSuite(oidcClient *oidc.Client, issuerURL string, userEmail string, accessToken string) (jose.Claims, error) {
client, err := a.newGsuiteClient(oidcClient, issuerURL, userEmail, accessToken)
func (a *AuthServer) claimsFromGSuite(config *jwt.Config, issuerURL string, userEmail string, domain string) (jose.Claims, error) {
client, err := a.newGsuiteClient(config, issuerURL, userEmail, domain)
if err != nil {
return nil, trace.Wrap(err)
}
return client.fetchGroups()
}

func (a *AuthServer) newGsuiteClient(oidcClient *oidc.Client, issuerURL string, userEmail string, accessToken string) (*gsuiteClient, error) {
func (a *AuthServer) newGsuiteClient(config *jwt.Config, issuerURL string, userEmail string, domain string) (*gsuiteClient, error) {
err := isHTTPS(issuerURL)
if err != nil {
return nil, trace.Wrap(err)
}

oac, err := oidcClient.OAuthClient()
if err != nil {
return nil, trace.Wrap(err)
}

u, err := url.Parse(teleport.GSuiteGroupsEndpoint)
if err != nil {
return nil, trace.Wrap(err)
}

return &gsuiteClient{
Client: oac.HttpClient(),
url: *u,
userEmail: userEmail,
accessToken: accessToken,
auditLog: a,
domain: domain,
client: config.Client(context.TODO()),
url: *u,
userEmail: userEmail,
config: config,
auditLog: a,
}, nil
}

type gsuiteClient struct {
phttp.Client
url url.URL
userEmail string
accessToken string
auditLog events.IAuditLog
client *http.Client
url url.URL
userEmail string
domain string
config *jwt.Config
auditLog events.IAuditLog
}

// fetchGroups fetches GSuite groups a user belongs to and returns
Expand Down Expand Up @@ -661,6 +659,7 @@ func (g *gsuiteClient) fetchGroupsPage(pageToken string) (*gsuiteGroups, error)
u := g.url
q := u.Query()
q.Set("userKey", g.userEmail)
q.Set("domain", g.domain)
if pageToken != "" {
q.Set("pageToken", pageToken)
}
Expand All @@ -673,9 +672,8 @@ func (g *gsuiteClient) fetchGroupsPage(pageToken string) (*gsuiteGroups, error)
if err != nil {
return nil, trace.Wrap(err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", g.accessToken))

resp, err := g.Do(req)
resp, err := g.client.Do(req)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -725,7 +723,7 @@ func mergeClaims(a jose.Claims, b jose.Claims) (jose.Claims, error) {
}

// getClaims gets claims from ID token and UserInfo and returns UserInfo claims merged into ID token claims.
func (a *AuthServer) getClaims(oidcClient *oidc.Client, issuerURL string, scope []string, code string) (jose.Claims, error) {
func (a *AuthServer) getClaims(oidcClient *oidc.Client, connector services.OIDCConnector, code string) (jose.Claims, error) {
var err error

oac, err := oidcClient.OAuthClient()
Expand All @@ -745,7 +743,7 @@ func (a *AuthServer) getClaims(oidcClient *oidc.Client, issuerURL string, scope
}
log.Debugf("OIDC ID Token claims: %v.", idTokenClaims)

userInfoClaims, err := claimsFromUserInfo(oidcClient, issuerURL, t.AccessToken)
userInfoClaims, err := claimsFromUserInfo(oidcClient, connector.GetIssuerURL(), t.AccessToken)
if err != nil {
if trace.IsNotFound(err) {
log.Debugf("OIDC provider doesn't offer UserInfo endpoint. Returning token claims: %v.", idTokenClaims)
Expand Down Expand Up @@ -783,12 +781,51 @@ func (a *AuthServer) getClaims(oidcClient *oidc.Client, issuerURL string, scope

// for GSuite users, fetch extra data from the proprietary google API
// only if scope includes admin groups readonly scope
if issuerURL == teleport.GSuiteIssuerURL && utils.SliceContainsStr(scope, teleport.GSuiteGroupsScope) {
if connector.GetIssuerURL() == teleport.GSuiteIssuerURL {
email, _, err := claims.StringClaim("email")
if err != nil {
return nil, trace.Wrap(err)
}
gsuiteClaims, err := a.claimsFromGSuite(oidcClient, issuerURL, email, t.AccessToken)

serviceAccountURI := connector.GetGoogleServiceAccountURI()
if serviceAccountURI == "" {
return nil, trace.NotFound(
"the gsuite connector requires google_service_account_uri parameter to be specified and pointing to a valid google service account file with credentials, read this article for more details https://developers.google.com/admin-sdk/directory/v1/guides/delegation")
}

uri, err := utils.ParseSessionsURI(serviceAccountURI)
if err != nil {
return nil, trace.BadParameter("failed to parse google_service_account_uri: %v", err)
}

impersonateAdmin := connector.GetGoogleAdminEmail()
if impersonateAdmin == "" {
return nil, trace.NotFound(
"the gsuite connector requires google_admin_email user to impersonate, as service accounts can not be used directly https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority")
}

jsonCredentials, err := ioutil.ReadFile(uri.Path)
if err != nil {
return nil, trace.Wrap(err)
}

config, err := google.JWTConfigFromJSON(jsonCredentials, teleport.GSuiteGroupsScope)
if err != nil {
return nil, trace.BadParameter("unable to parse client secret file to config: %v", err)
}
// User should impersonate admin user, otherwise it won't work:
//
// https://developers.google.com/admin-sdk/directory/v1/guides/delegation
//
// "Note: Only users with access to the Admin APIs can access the Admin SDK Directory API, therefore your service account needs to impersonate one of those users to access the Admin SDK Directory API. Additionally, the user must have logged in at least once and accepted the G Suite Terms of Service."
//
domain, exists, err := userInfoClaims.StringClaim(teleport.GSuiteDomainClaim)
if err != nil || !exists {
return nil, trace.BadParameter("hd is the required claim for GSuite")
}
config.Subject = impersonateAdmin

gsuiteClaims, err := a.claimsFromGSuite(config, connector.GetIssuerURL(), email, domain)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
Expand Down
35 changes: 35 additions & 0 deletions lib/services/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ type OIDCConnector interface {
SetClaimsToRoles([]ClaimMapping)
// SetDisplay sets friendly name for this provider.
SetDisplay(string)
// GetGoogleServiceAccountURI returns path to google service account URI
GetGoogleServiceAccountURI() string
// GetGoogleAdminEmail returns a google admin user email
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
// "Note: Although you can use service accounts in applications that run from a G Suite domain, service accounts are not members of your G Suite account and aren’t subject to domain policies set by G Suite administrators. For example, a policy set in the G Suite admin console to restrict the ability of G Suite end users to share documents outside of the domain would not apply to service accounts."
GetGoogleAdminEmail() string
}

// NewOIDCConnector returns a new OIDCConnector based off a name and OIDCConnectorSpecV2.
Expand Down Expand Up @@ -233,6 +239,16 @@ type OIDCConnectorV2 struct {
Spec OIDCConnectorSpecV2 `json:"spec"`
}

// GetGoogleServiceAccountFile returns an optional path to google service account file
func (o *OIDCConnectorV2) GetGoogleServiceAccountURI() string {
return o.Spec.GoogleServiceAccountURI
}

// GetGoogleAdminEmail returns a google admin user email
func (o *OIDCConnectorV2) GetGoogleAdminEmail() string {
return o.Spec.GoogleAdminEmail
}

// GetVersion returns resource version
func (o *OIDCConnectorV2) GetVersion() string {
return o.Version
Expand Down Expand Up @@ -525,6 +541,19 @@ func (o *OIDCConnectorV2) Check() error {
}
}

if o.Spec.GoogleServiceAccountURI != "" {
uri, err := utils.ParseSessionsURI(o.Spec.GoogleServiceAccountURI)
if err != nil {
return trace.Wrap(err)
}
if uri.Scheme != teleport.SchemeFile {
return trace.BadParameter("only %v:// scheme is supported for google_service_account_uri", teleport.SchemeFile)
}
if o.Spec.GoogleAdminEmail == "" {
return trace.BadParameter("whenever google_service_account_uri is specified, google_admin_email should be set as well, read https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority for more details")
}
}

return nil
}

Expand Down Expand Up @@ -581,6 +610,10 @@ type OIDCConnectorSpecV2 struct {
Scope []string `json:"scope,omitempty"`
// ClaimsToRoles specifies dynamic mapping from claims to roles
ClaimsToRoles []ClaimMapping `json:"claims_to_roles,omitempty"`
// GoogleServiceAccountURI is a path to google service account uri
GoogleServiceAccountURI string `json:"google_service_account_uri,omitempty"`
// GoogleAdminEmail is email of google admin to impersonate
GoogleAdminEmail string `json:"google_admin_email,omitempty"`
}

// OIDCConnectorSpecV2Schema is a JSON Schema for OIDC Connector
Expand All @@ -596,6 +629,8 @@ var OIDCConnectorSpecV2Schema = fmt.Sprintf(`{
"acr_values": {"type": "string"},
"provider": {"type": "string"},
"display": {"type": "string"},
"google_service_account_uri": {"type": "string"},
"google_admin_email": {"type": "string"},
"scope": {
"type": "array",
"items": {
Expand Down
Loading