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

Fetch groups for GSuite SSO. #2456

Merged
merged 1 commit into from
Jan 17, 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
9 changes: 9 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,15 @@ const (
TraitInternalKubeGroupsVariable = "{{internal.kubernetes_groups}}"
)

const (
// GSuiteIssuerURL is issuer URL used for GSuite provider
GSuiteIssuerURL = "https://accounts.google.com"
// GSuiteGroupsEndpoint is gsuite API endpoint
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"
)

// SCP is Secure Copy.
const SCP = "scp"

Expand Down
164 changes: 161 additions & 3 deletions lib/auth/oidc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2017 Gravitational, Inc.
Copyright 2017-2019 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,7 @@ package auth
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
Expand All @@ -29,6 +30,7 @@ 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"
Expand Down Expand Up @@ -175,7 +177,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(), code)
claims, err := a.getClaims(oidcClient, connector.GetIssuerURL(), connector.GetScope(), code)
if err != nil {
return nil, trace.OAuth2(
oauth2.ErrorUnsupportedResponseType, "unable to construct claims", q)
Expand Down Expand Up @@ -476,6 +478,138 @@ 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)
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) {
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,
}, nil
}

type gsuiteClient struct {
phttp.Client
url url.URL
userEmail string
accessToken string
auditLog events.IAuditLog
}

// fetchGroups fetches GSuite groups a user belongs to and returns
// "groups" claim with
func (g *gsuiteClient) fetchGroups() (jose.Claims, error) {
count := 0
var groups []string
var nextPageToken string
collect:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd break this out into it's own function, seems like a lot packed into a single function.

for {
if count > MaxPages {
warningMessage := "Truncating list of teams used to populate claims: " +
"hit maximum number pages that can be fetched from GSuite."

// Print warning to Teleport logs as well as the Audit Log.
log.Warnf(warningMessage)
g.auditLog.EmitAuditEvent(events.UserLoginEvent, events.EventFields{
events.LoginMethod: events.LoginMethodOIDC,
events.AuthAttemptMessage: warningMessage,
})
break collect
}
response, err := g.fetchGroupsPage(nextPageToken)
if err != nil {
return nil, trace.Wrap(err)
}
groups = append(groups, response.groups()...)
if response.NextPageToken == "" {
break collect
}
count++
nextPageToken = response.NextPageToken
}
return jose.Claims{"groups": groups}, nil
}

func (g *gsuiteClient) fetchGroupsPage(pageToken string) (*gsuiteGroups, error) {
// copy URL to avoid modifying the same url
// with query parameters
u := g.url
q := u.Query()
q.Set("userKey", g.userEmail)
if pageToken != "" {
q.Set("pageToken", pageToken)
}
u.RawQuery = q.Encode()
endpoint := u.String()

log.Debugf("Fetching OIDC claims from GSuite groups endpoint: %q.", endpoint)

req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return nil, trace.Wrap(err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", g.accessToken))

resp, err := g.Do(req)
if err != nil {
return nil, trace.Wrap(err)
}
defer resp.Body.Close()

bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, trace.Wrap(err)
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, trace.AccessDenied("bad status code: %v %v", resp.StatusCode, string(bytes))
}
var response gsuiteGroups
if err := json.Unmarshal(bytes, &response); err != nil {
return nil, trace.BadParameter("failed to parse response: %v", err)
}
return &response, nil
}

type gsuiteGroups struct {
NextPageToken string `json:"nextPageToken"`
Groups []gsuiteGroup `json:"groups"`
}

func (g gsuiteGroups) groups() []string {
groups := make([]string, len(g.Groups))
for i, group := range g.Groups {
groups[i] = group.Email
}
return groups
}

type gsuiteGroup struct {
Email string `json:"email"`
}

// mergeClaims merges b into a.
func mergeClaims(a jose.Claims, b jose.Claims) (jose.Claims, error) {
for k, v := range b {
Expand All @@ -489,7 +623,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, code string) (jose.Claims, error) {
func (a *AuthServer) getClaims(oidcClient *oidc.Client, issuerURL string, scope []string, code string) (jose.Claims, error) {
var err error

oac, err := oidcClient.OAuthClient()
Expand Down Expand Up @@ -545,6 +679,30 @@ func (a *AuthServer) getClaims(oidcClient *oidc.Client, issuerURL string, code s
return nil, trace.Wrap(err)
}

// 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) {
email, _, err := claims.StringClaim("email")
if err != nil {
return nil, trace.Wrap(err)
}
gsuiteClaims, err := a.claimsFromGSuite(oidcClient, issuerURL, email, t.AccessToken)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
log.Debugf("Found no GSuite claims.")
} else {
if gsuiteClaims != nil {
log.Debugf("Got GSuiteclaims: %v.", gsuiteClaims)
}
claims, err = mergeClaims(claims, gsuiteClaims)
if err != nil {
return nil, trace.Wrap(err)
}
}
}

return claims, nil
}

Expand Down