"time"
"git.arvados.org/arvados.git/lib/controller/api"
- "git.arvados.org/arvados.git/lib/controller/railsproxy"
"git.arvados.org/arvados.git/lib/controller/rpc"
"git.arvados.org/arvados.git/lib/ctrlctx"
"git.arvados.org/arvados.git/sdk/go/arvados"
"google.golang.org/api/people/v1"
)
-const (
+var (
tokenCacheSize = 1000
tokenCacheNegativeTTL = time.Minute * 5
tokenCacheTTL = time.Minute * 10
+ tokenCacheRaceWindow = time.Minute
)
type oidcLoginController struct {
Cluster *arvados.Cluster
- RailsProxy *railsProxy
+ Parent *Conn
Issuer string // OIDC issuer URL, e.g., "https://accounts.google.com"
ClientID string
ClientSecret string
- UseGooglePeopleAPI bool // Use Google People API to look up alternate email addresses
- 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
+ UseGooglePeopleAPI bool // Use Google People API to look up alternate email addresses
+ 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
+ AuthParams map[string]string // Additional parameters to pass with authentication request
// override Google People API base URL for testing purposes
// (normally empty, set by google pkg to
return loginError(errors.New("missing return_to parameter"))
}
state := ctrl.newOAuth2State([]byte(ctrl.Cluster.SystemRootToken), opts.Remote, opts.ReturnTo)
+ var authparams []oauth2.AuthCodeOption
+ for k, v := range ctrl.AuthParams {
+ authparams = append(authparams, oauth2.SetAuthURLParam(k, v))
+ }
return arvados.LoginResponse{
- RedirectLocation: ctrl.oauth2conf.AuthCodeURL(state.String(),
- // prompt=select_account tells Google
- // to show the "choose which Google
- // account" page, even if the client
- // is currently logged in to exactly
- // one Google account.
- oauth2.SetAuthURLParam("prompt", "select_account")),
+ RedirectLocation: ctrl.oauth2conf.AuthCodeURL(state.String(), authparams...),
}, nil
}
// Callback after OIDC sign-in.
return loginError(err)
}
ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{ctrl.Cluster.SystemRootToken}})
- return ctrl.RailsProxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
+ return ctrl.Parent.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
ReturnTo: state.Remote + "," + state.ReturnTo,
AuthInfo: *authinfo,
})
if names := strings.Fields(strings.TrimSpace(name)); len(names) > 1 {
ret.FirstName = strings.Join(names[0:len(names)-1], " ")
ret.LastName = names[len(names)-1]
- } else {
+ } else if len(names) > 0 {
ret.FirstName = names[0]
}
ret.Email, _ = claims[ctrl.EmailClaim].(string)
// We want ctrl to be nil if the chosen controller is not a
// *oidcLoginController, so we can ignore the 2nd return value
// of this type cast.
- ctrl, _ := chooseLoginController(cluster, railsproxy.NewConn(cluster)).(*oidcLoginController)
+ ctrl, _ := NewConn(cluster).loginController.(*oidcLoginController)
cache, err := lru.New2Q(tokenCacheSize)
if err != nil {
panic(err)
return origFunc(ctx, opts)
}
// Check each token in the incoming request. If any
- // are OAuth2 access tokens, swap them out for Arvados
- // tokens.
+ // are valid OAuth2 access tokens, insert/update them
+ // in the database so RailsAPI's auth code accepts
+ // them.
for _, tok := range creds.Tokens {
err = ta.registerToken(ctx, tok)
if err != nil {
// if so, ensures that an api_client_authorizations row exists so that
// RailsAPI will accept it as an Arvados token.
func (ta *oidcTokenAuthorizer) registerToken(ctx context.Context, tok string) error {
- if strings.HasPrefix(tok, "v2/") {
+ if tok == ta.ctrl.Cluster.SystemRootToken || strings.HasPrefix(tok, "v2/") {
return nil
}
if cached, hit := ta.cache.Get(tok); !hit {
// cached negative result (value is expiry time)
if time.Now().Before(exp) {
return nil
- } else {
- ta.cache.Remove(tok)
}
+ ta.cache.Remove(tok)
} else {
// cached positive result
- aca := cached.(*arvados.APIClientAuthorization)
+ aca := cached.(arvados.APIClientAuthorization)
var expiring bool
if aca.ExpiresAt != "" {
t, err := time.Parse(time.RFC3339Nano, aca.ExpiresAt)
// Expiry time for our token is one minute longer than our
// cache TTL, so we don't pass it through to RailsAPI just as
// it's expiring.
- exp := time.Now().Add(tokenCacheTTL + time.Minute)
+ exp := time.Now().UTC().Add(tokenCacheTTL + tokenCacheRaceWindow)
var aca arvados.APIClientAuthorization
if updating {
}
ctxlog.FromContext(ctx).WithField("HMAC", hmac).Debug("(*oidcTokenAuthorizer)registerToken: updated api_client_authorizations row")
} else {
- aca, err = createAPIClientAuthorization(ctx, ta.ctrl.RailsProxy, ta.ctrl.Cluster.SystemRootToken, *authinfo)
+ aca, err = ta.ctrl.Parent.CreateAPIClientAuthorization(ctx, ta.ctrl.Cluster.SystemRootToken, *authinfo)
if err != nil {
return err
}
if err != nil {
return err
}
+ aca.ExpiresAt = exp.Format(time.RFC3339Nano)
ta.cache.Add(tok, aca)
return nil
}