diff --git a/constants.go b/constants.go index ed60a94c5029..195d233cf1a0 100644 --- a/constants.go +++ b/constants.go @@ -361,6 +361,18 @@ const ( DebugLevel = "debug" ) +const ( + // These values are from https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + + // OIDCPromptSelectAccount instructs the Authorization Server to + // prompt the End-User to select a user account. + OIDCPromptSelectAccount = "select_account" + + // OIDCAccessTypeOnline indicates that OIDC flow should be performed + // with Authorization server and user connected online + OIDCAccessTypeOnline = "online" +) + // Component generates "component:subcomponent1:subcomponent2" strings used // in debugging func Component(components ...string) string { diff --git a/lib/auth/oidc.go b/lib/auth/oidc.go index fb4bc2c981ae..79a35a3feb40 100644 --- a/lib/auth/oidc.go +++ b/lib/auth/oidc.go @@ -163,8 +163,9 @@ func (s *AuthServer) CreateOIDCAuthRequest(req services.OIDCAuthRequest) (*servi } req.StateToken = stateToken - // online is OIDC online scope, "select_account" forces user to always select account - req.RedirectURL = oauthClient.AuthCodeURL(req.StateToken, "online", "select_account") + + // online indicates that this login should only work online + req.RedirectURL = oauthClient.AuthCodeURL(req.StateToken, teleport.OIDCAccessTypeOnline, connector.GetPrompt()) // if the connector has an Authentication Context Class Reference (ACR) value set, // update redirect url and add it as a query value. diff --git a/lib/services/oidc.go b/lib/services/oidc.go index 66fe65b3f0c4..5af7466b3c8a 100644 --- a/lib/services/oidc.go +++ b/lib/services/oidc.go @@ -76,6 +76,10 @@ type OIDCConnector interface { SetIssuerURL(string) // SetRedirectURL sets RedirectURL SetRedirectURL(string) + // SetPrompt sets OIDC prompt value + SetPrompt(string) + // GetPrompt returns OIDC prompt value, + GetPrompt() string // SetACR sets the Authentication Context Class Reference (ACR) value. SetACR(string) // SetProvider sets the identity provider. @@ -239,7 +243,23 @@ type OIDCConnectorV2 struct { Spec OIDCConnectorSpecV2 `json:"spec"` } -// GetGoogleServiceAccountFile returns an optional path to google service account file +// SetPrompt sets OIDC prompt value +func (o *OIDCConnectorV2) SetPrompt(p string) { + o.Spec.Prompt = &p +} + +// GetPrompt returns OIDC prompt value, +// * if not set, in this case defaults to select_account for backwards compatibility +// * set to empty string, in this case it will be omitted +// * and any non empty value, passed as is +func (o *OIDCConnectorV2) GetPrompt() string { + if o.Spec.Prompt == nil { + return teleport.OIDCPromptSelectAccount + } + return *o.Spec.Prompt +} + +// GetGoogleServiceAccountURI returns an optional path to google service account file func (o *OIDCConnectorV2) GetGoogleServiceAccountURI() string { return o.Spec.GoogleServiceAccountURI } @@ -586,7 +606,10 @@ const OIDCConnectorV2SchemaTemplate = `{ }` // OIDCConnectorSpecV2 specifies configuration for Open ID Connect compatible external -// identity provider, e.g. google in some organisation +// identity provider: +// +// https://openid.net/specs/openid-connect-core-1_0.html +// type OIDCConnectorSpecV2 struct { // Issuer URL is the endpoint of the provider, e.g. https://accounts.google.com IssuerURL string `json:"issuer_url"` @@ -608,6 +631,10 @@ type OIDCConnectorSpecV2 struct { Display string `json:"display,omitempty"` // Scope is additional scopes set by provder Scope []string `json:"scope,omitempty"` + // Prompt is optional OIDC prompt, empty string omits prompt + // if not specified, defaults to select_account for backwards compatibility + // otherwise, is set to a value specified in this field + Prompt *string `json:"prompt,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 @@ -629,6 +656,7 @@ var OIDCConnectorSpecV2Schema = fmt.Sprintf(`{ "acr_values": {"type": "string"}, "provider": {"type": "string"}, "display": {"type": "string"}, + "prompt": {"type": "string"}, "google_service_account_uri": {"type": "string"}, "google_admin_email": {"type": "string"}, "scope": { diff --git a/lib/services/oidc_test.go b/lib/services/oidc_test.go index 30a3190c99d7..b4ea6ec37703 100644 --- a/lib/services/oidc_test.go +++ b/lib/services/oidc_test.go @@ -1,5 +1,5 @@ /* -Copyright 2017 Gravitational, Inc. +Copyright 2017-2020 Gravitational, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package services import ( "fmt" + "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/utils" "gopkg.in/check.v1" @@ -79,6 +80,72 @@ func (s *OIDCSuite) TestUnmarshal(c *check.C) { c.Assert(oc.GetClientID(), check.Equals, "id-from-google.apps.googleusercontent.com") c.Assert(oc.GetRedirectURL(), check.Equals, "https://localhost:3080/v1/webapi/oidc/callback") c.Assert(oc.GetDisplay(), check.Equals, "whatever") + c.Assert(oc.GetPrompt(), check.Equals, teleport.OIDCPromptSelectAccount) +} + +// TestUnmarshalEmptyPrompt makes sure that empty prompt value +// that is set does not default to select_account +func (s *OIDCSuite) TestUnmarshalEmptyPrompt(c *check.C) { + input := ` + { + "kind": "oidc", + "version": "v2", + "metadata": { + "name": "google" + }, + "spec": { + "issuer_url": "https://accounts.google.com", + "client_id": "id-from-google.apps.googleusercontent.com", + "client_secret": "secret-key-from-google", + "redirect_url": "https://localhost:3080/v1/webapi/oidc/callback", + "display": "whatever", + "scope": ["roles"], + "prompt": "" + } + } + ` + + oc, err := GetOIDCConnectorMarshaler().UnmarshalOIDCConnector([]byte(input)) + c.Assert(err, check.IsNil) + + c.Assert(oc.GetName(), check.Equals, "google") + c.Assert(oc.GetIssuerURL(), check.Equals, "https://accounts.google.com") + c.Assert(oc.GetClientID(), check.Equals, "id-from-google.apps.googleusercontent.com") + c.Assert(oc.GetRedirectURL(), check.Equals, "https://localhost:3080/v1/webapi/oidc/callback") + c.Assert(oc.GetDisplay(), check.Equals, "whatever") + c.Assert(oc.GetPrompt(), check.Equals, "") +} + +// TestUnmarshalPromptValue makes sure that prompt value is set properly +func (s *OIDCSuite) TestUnmarshalPromptValue(c *check.C) { + input := ` + { + "kind": "oidc", + "version": "v2", + "metadata": { + "name": "google" + }, + "spec": { + "issuer_url": "https://accounts.google.com", + "client_id": "id-from-google.apps.googleusercontent.com", + "client_secret": "secret-key-from-google", + "redirect_url": "https://localhost:3080/v1/webapi/oidc/callback", + "display": "whatever", + "scope": ["roles"], + "prompt": "consent login" + } + } + ` + + oc, err := GetOIDCConnectorMarshaler().UnmarshalOIDCConnector([]byte(input)) + c.Assert(err, check.IsNil) + + c.Assert(oc.GetName(), check.Equals, "google") + c.Assert(oc.GetIssuerURL(), check.Equals, "https://accounts.google.com") + c.Assert(oc.GetClientID(), check.Equals, "id-from-google.apps.googleusercontent.com") + c.Assert(oc.GetRedirectURL(), check.Equals, "https://localhost:3080/v1/webapi/oidc/callback") + c.Assert(oc.GetDisplay(), check.Equals, "whatever") + c.Assert(oc.GetPrompt(), check.Equals, "consent login") } func (s *OIDCSuite) TestUnmarshalInvalid(c *check.C) {