mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 17:23:22 +00:00
Fix support for GSuite logins
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: "admin@example.com" claims_to_roles: - {claim: "groups", value: "admin@example.com", roles: ["clusteradmin"]} ```
This commit is contained in:
parent
6b0bc77ce6
commit
21e0342021
3
Gopkg.lock
generated
3
Gopkg.lock
generated
|
@ -1398,7 +1398,6 @@
|
|||
"github.com/aws/aws-sdk-go/service/s3/s3manager",
|
||||
"github.com/beevik/etree",
|
||||
"github.com/codahale/hdrhistogram",
|
||||
"github.com/coreos/go-oidc/http",
|
||||
"github.com/coreos/go-oidc/jose",
|
||||
"github.com/coreos/go-oidc/oauth2",
|
||||
"github.com/coreos/go-oidc/oidc",
|
||||
|
@ -1470,6 +1469,8 @@
|
|||
"golang.org/x/net/http2",
|
||||
"golang.org/x/net/proxy",
|
||||
"golang.org/x/net/websocket",
|
||||
"golang.org/x/oauth2/google",
|
||||
"golang.org/x/oauth2/jwt",
|
||||
"golang.org/x/text/encoding",
|
||||
"golang.org/x/text/encoding/unicode",
|
||||
"google.golang.org/api/iterator",
|
||||
|
|
|
@ -439,6 +439,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.
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
|
Loading…
Reference in a new issue