h3. Using an OpenID Connect access token
-A cluster that uses OpenID Connect as a login provider can be configured to accept OIDC access tokens as well as Arvados API tokens (this is disabled by default; see @Login.OpenIDConnect.AcceptAccessTokenScope@ in the "default config.yml file":{{site.baseurl}}/admin/config.html).
+A cluster that uses OpenID Connect as a login provider can be configured to accept OIDC access tokens as well as Arvados API tokens (this is disabled by default; see @Login.OpenIDConnect.AcceptAccessToken@ in the "default config.yml file":{{site.baseurl}}/admin/config.html).
# The client obtains an access token from the OpenID Connect provider via some method outside of Arvados.
# The client presents the access token with an Arvados API request (e.g., request header @Authorization: Bearer xxxxaccesstokenxxxx@).
-# Depending on configuration, the API server decodes the access token (which must be a signed JWT) and confirms that it includes the required scope.
+# Depending on configuration, the API server decodes the access token (which must be a signed JWT) and confirms that it includes the required scope (see @Login.OpenIDConnect.AcceptAccessTokenScope@ in the "default config.yml file":{{site.baseurl}}/admin/config.html).
# The API server uses the provider's UserInfo endpoint to validate the presented token.
# If the token is valid, it is cached in the Arvados database and accepted in subsequent API calls for the next 10 minutes.
AuthenticationRequestParameters:
SAMPLE: ""
- # Accept an OIDC access token as an API token if it is a JWT
- # whose "scope" value includes this scope. To accept any
- # access token (even if it's not a JWT), use "*". To disable
- # this feature, use the empty string "".
+ # Accept an OIDC access token as an API token if the OIDC
+ # provider's UserInfo endpoint accepts it.
#
- # If an incoming token's scope is satisfactory, Arvados
- # verifies the token is valid by presenting it at the OIDC
- # provider's UserInfo endpoint. (Signature and expiry are not
- # checked separately.) Valid tokens are cached for 10 minutes.
+ # AcceptAccessTokenScope should also be used when enabling
+ # this feature.
+ AcceptAccessToken: false
+
+ # Before accepting an OIDC access token as an API token, first
+ # check that it is a JWT whose "scope" value includes this
+ # value. Example: "https://zzzzz.example.com/" (your Arvados
+ # API endpoint).
+ #
+ # If this value is empty and AcceptAccessToken is true, all
+ # access tokens will be accepted regardless of scope,
+ # including non-JWT tokens. This is not recommended.
AcceptAccessTokenScope: ""
PAM:
"Login.LDAP.UsernameAttribute": false,
"Login.LoginCluster": true,
"Login.OpenIDConnect": true,
+ "Login.OpenIDConnect.AcceptAccessToken": false,
"Login.OpenIDConnect.AcceptAccessTokenScope": false,
"Login.OpenIDConnect.AuthenticationRequestParameters": false,
"Login.OpenIDConnect.ClientID": false,
AuthenticationRequestParameters:
SAMPLE: ""
- # Accept an OIDC access token as an API token if it is a JWT
- # whose "scope" value includes this scope. To accept any
- # access token (even if it's not a JWT), use "*". To disable
- # this feature, use the empty string "".
+ # Accept an OIDC access token as an API token if the OIDC
+ # provider's UserInfo endpoint accepts it.
#
- # If an incoming token's scope is satisfactory, Arvados
- # verifies the token is valid by presenting it at the OIDC
- # provider's UserInfo endpoint. (Signature and expiry are not
- # checked separately.) Valid tokens are cached for 10 minutes.
+ # AcceptAccessTokenScope should also be used when enabling
+ # this feature.
+ AcceptAccessToken: false
+
+ # Before accepting an OIDC access token as an API token, first
+ # check that it is a JWT whose "scope" value includes this
+ # value. Example: "https://zzzzz.example.com/" (your Arvados
+ # API endpoint).
+ #
+ # If this value is empty and AcceptAccessToken is true, all
+ # access tokens will be accepted regardless of scope,
+ # including non-JWT tokens. This is not recommended.
AcceptAccessTokenScope: ""
PAM:
cluster.Login.OpenIDConnect.ClientSecret = s.fakeProvider.ValidClientSecret
cluster.Login.OpenIDConnect.EmailClaim = "email"
cluster.Login.OpenIDConnect.EmailVerifiedClaim = "email_verified"
- cluster.Login.OpenIDConnect.AcceptAccessTokenScope = "*"
+ cluster.Login.OpenIDConnect.AcceptAccessToken = true
+ cluster.Login.OpenIDConnect.AcceptAccessTokenScope = ""
s.testHandler = &Handler{Cluster: cluster}
s.testServer = newServerFromIntegrationTestEnv(c)
ClientSecret: ` + s.oidcprovider.ValidClientSecret + `
EmailClaim: email
EmailVerifiedClaim: email_verified
- AcceptAccessTokenScope: "*"
+ AcceptAccessToken: true
+ AcceptAccessTokenScope: ""
`
} else {
yaml += `
EmailClaim: cluster.Login.OpenIDConnect.EmailClaim,
EmailVerifiedClaim: cluster.Login.OpenIDConnect.EmailVerifiedClaim,
UsernameClaim: cluster.Login.OpenIDConnect.UsernameClaim,
+ AcceptAccessToken: cluster.Login.OpenIDConnect.AcceptAccessToken,
AcceptAccessTokenScope: cluster.Login.OpenIDConnect.AcceptAccessTokenScope,
}
case wantSSO:
EmailClaim string // OpenID claim to use as email address; typically "email"
EmailVerifiedClaim string // If non-empty, ensure claim value is true before accepting EmailClaim; typically "email_verified"
UsernameClaim string // If non-empty, use as preferred username
- AcceptAccessTokenScope string // If non-empty, accept any access token containing this scope as an API token
+ AcceptAccessToken bool // Accept access tokens as API tokens
+ AcceptAccessTokenScope string // If non-empty, don't accept access tokens as API tokens unless they contain this scope
AuthParams map[string]string // Additional parameters to pass with authentication request
// override Google People API base URL for testing purposes
// return a 403 error, otherwise true (acceptable as an API token) or
// false (pass through unmodified).
//
+// Return false if configured not to accept access tokens at all.
+//
// Note we don't check signature or expiry here. We are relying on the
// caller to verify those separately (e.g., by calling the UserInfo
// endpoint).
func (ta *oidcTokenAuthorizer) checkAccessTokenScope(ctx context.Context, tok string) (bool, error) {
- switch ta.ctrl.AcceptAccessTokenScope {
- case "*":
- return true, nil
- case "":
+ if !ta.ctrl.AcceptAccessToken {
return false, nil
+ } else if ta.ctrl.AcceptAccessTokenScope == "" {
+ return true, nil
}
var claims struct {
Scope string `json:"scope"`
json.Unmarshal([]byte(fmt.Sprintf("%q", s.fakeProvider.Issuer.URL)), &s.cluster.Login.OpenIDConnect.Issuer)
s.cluster.Login.OpenIDConnect.ClientID = "oidc#client#id"
s.cluster.Login.OpenIDConnect.ClientSecret = "oidc#client#secret"
- s.cluster.Login.OpenIDConnect.AcceptAccessTokenScope = "*"
+ s.cluster.Login.OpenIDConnect.AcceptAccessToken = true
+ s.cluster.Login.OpenIDConnect.AcceptAccessTokenScope = ""
s.fakeProvider.ValidClientID = "oidc#client#id"
s.fakeProvider.ValidClientSecret = "oidc#client#secret"
db := arvadostest.DB(c, s.cluster)
apiToken = fmt.Sprintf("%x", mac.Sum(nil))
for _, trial := range []struct {
- configScope string
- acceptable bool
- shouldRun bool
+ configEnable bool
+ configScope string
+ acceptable bool
+ shouldRun bool
}{
- {"foobar", true, true},
- {"foo", false, false},
- {"*", true, true},
- {"", false, true},
+ {true, "foobar", true, true},
+ {true, "foo", false, false},
+ {true, "", true, true},
+ {false, "", false, true},
+ {false, "foobar", false, true},
} {
c.Logf("trial = %+v", trial)
cleanup()
+ s.cluster.Login.OpenIDConnect.AcceptAccessToken = trial.configEnable
s.cluster.Login.OpenIDConnect.AcceptAccessTokenScope = trial.configScope
oidcAuthorizer = OIDCAccessTokenAuthorizer(s.cluster, func(context.Context) (*sqlx.DB, error) { return db, nil })
checked := false
EmailClaim string
EmailVerifiedClaim string
UsernameClaim string
+ AcceptAccessToken bool
AcceptAccessTokenScope string
AuthenticationRequestParameters map[string]string
}