Skip to content

Commit

Permalink
Fix support for GSuite logins
Browse files Browse the repository at this point in the history
This commit fixes support for GSuite logins
by using service accounts for access purposes.

The resulting connector now looks like:

```yaml
kind: oidc
version: v2
metadata:
  name: gsuite
spec:
  redirect_url: https://example.com/v1/webapi/oidc/callback
  client_id: exampleclientid.apps.googleusercontent.com
  client_secret: exampleclientsecret
  issuer_url: https://accounts.google.com
  # Notice that scope here is not requiested from OIDC exchange anymore, this scope
  #
  # https://www.googleapis.com/auth/admin.directory.group.readonly
  #
  # is now implicitly requested by the client
  #
  scope: ['openid', 'email']
  # The setup below is involved and requires careful following of the guides:
  #
  # https://developers.google.com/admin-sdk/directory/v1/guides/delegation
  # https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
  #
  # The service account scopes have to be set to
  #
  # https://www.googleapis.com/auth/admin.directory.group.readonly
  # https://www.googleapis.com/auth/admin.directory.group.member.readonly
  #
  # the following paths are supported:
  # 1. plain path
  # /var/lib/secrets/gsuite-creds.json
  #
  # 2. explicit scheme file://
  # file:///var/lib/secrets/gsuite-creds.json
  #
  # other schemes are not supported at the moment
  #
  google_service_account_file: "/var/lib/secrets/gsuite-creds.json"
  google_admin_email: "[email protected]"
  claims_to_roles:
    - {claim: "groups", value: "[email protected]", roles: ["clusteradmin"]}
```
  • Loading branch information
klizhentas committed Nov 5, 2019
1 parent 5a8ae6b commit 762db69
Show file tree
Hide file tree
Showing 59 changed files with 14,162 additions and 28 deletions.
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

0 comments on commit 762db69

Please sign in to comment.