16669: Accept OIDC access token in RailsAPI auth.
authorTom Clegg <tom@tomclegg.ca>
Wed, 16 Sep 2020 14:17:31 +0000 (10:17 -0400)
committerTom Clegg <tom@tomclegg.ca>
Wed, 21 Oct 2020 13:04:25 +0000 (09:04 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@tomclegg.ca>

lib/controller/localdb/login_oidc.go
services/api/app/middlewares/arvados_api_token.rb
services/api/app/models/api_client_authorization.rb

index 60de70b5d2e51107e48649d615e1caf5e19c3f5e..9188f0eed997f935c987b5529e6a6dd55d0c4c3c 100644 (file)
@@ -32,6 +32,7 @@ import (
        "github.com/coreos/go-oidc"
        lru "github.com/hashicorp/golang-lru"
        "github.com/jmoiron/sqlx"
+       "github.com/sirupsen/logrus"
        "golang.org/x/oauth2"
        "google.golang.org/api/option"
        "google.golang.org/api/people/v1"
@@ -341,12 +342,11 @@ type oidcTokenAuthorizer struct {
 
 func (ta *oidcTokenAuthorizer) Middleware(w http.ResponseWriter, r *http.Request, next http.Handler) {
        if authhdr := strings.Split(r.Header.Get("Authorization"), " "); len(authhdr) > 1 && (authhdr[0] == "OAuth2" || authhdr[0] == "Bearer") {
-               tok, err := ta.exchangeToken(r.Context(), authhdr[1])
+               err := ta.registerToken(r.Context(), authhdr[1])
                if err != nil {
                        http.Error(w, err.Error(), http.StatusInternalServerError)
                        return
                }
-               r.Header.Set("Authorization", "Bearer "+tok)
        }
        next.ServeHTTP(w, r)
 }
@@ -364,22 +364,22 @@ func (ta *oidcTokenAuthorizer) WrapCalls(origFunc api.RoutableFunc) api.Routable
                // Check each token in the incoming request. If any
                // are OAuth2 access tokens, swap them out for Arvados
                // tokens.
-               for tokidx, tok := range creds.Tokens {
-                       tok, err = ta.exchangeToken(ctx, tok)
+               for _, tok := range creds.Tokens {
+                       err = ta.registerToken(ctx, tok)
                        if err != nil {
                                return nil, err
                        }
-                       creds.Tokens[tokidx] = tok
                }
-               ctxlog.FromContext(ctx).WithField("creds", creds).Debug("(*oidcTokenAuthorizer)WrapCalls: new creds")
-               ctx = auth.NewContext(ctx, creds)
                return origFunc(ctx, opts)
        }
 }
 
-func (ta *oidcTokenAuthorizer) exchangeToken(ctx context.Context, tok string) (string, error) {
+// registerToken checks whether tok is a valid OIDC Access Token and,
+// 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/") {
-               return tok, nil
+               return nil
        }
        if cached, hit := ta.cache.Get(tok); !hit {
                // Fall through to database and OIDC provider checks
@@ -387,7 +387,7 @@ func (ta *oidcTokenAuthorizer) exchangeToken(ctx context.Context, tok string) (s
        } else if exp, ok := cached.(time.Time); ok {
                // cached negative result (value is expiry time)
                if time.Now().Before(exp) {
-                       return tok, nil
+                       return nil
                } else {
                        ta.cache.Remove(tok)
                }
@@ -398,22 +398,22 @@ func (ta *oidcTokenAuthorizer) exchangeToken(ctx context.Context, tok string) (s
                if aca.ExpiresAt != "" {
                        t, err := time.Parse(time.RFC3339Nano, aca.ExpiresAt)
                        if err != nil {
-                               return "", fmt.Errorf("error parsing expires_at value: %w", err)
+                               return fmt.Errorf("error parsing expires_at value: %w", err)
                        }
                        expiring = t.Before(time.Now().Add(time.Minute))
                }
                if !expiring {
-                       return aca.TokenV2(), nil
+                       return nil
                }
        }
 
        db, err := ta.getdb(ctx)
        if err != nil {
-               return "", err
+               return err
        }
        tx, err := db.Beginx()
        if err != nil {
-               return "", err
+               return err
        }
        defer tx.Rollback()
        ctx = ctrlctx.NewWithTransaction(ctx, tx)
@@ -428,13 +428,13 @@ func (ta *oidcTokenAuthorizer) exchangeToken(ctx context.Context, tok string) (s
        var expiring bool
        err = tx.QueryRowContext(ctx, `select (expires_at is not null and expires_at - interval '1 minute' <= current_timestamp at time zone 'UTC') from api_client_authorizations where api_token=$1`, hmac).Scan(&expiring)
        if err != nil && err != sql.ErrNoRows {
-               return "", fmt.Errorf("database error while checking token: %w", err)
+               return fmt.Errorf("database error while checking token: %w", err)
        } else if err == nil && !expiring {
                // Token is already in the database as an Arvados
                // token, and isn't about to expire, so we can pass it
                // through to RailsAPI etc. regardless of whether it's
                // an OIDC access token.
-               return tok, nil
+               return nil
        }
        updating := err == nil
 
@@ -444,7 +444,7 @@ func (ta *oidcTokenAuthorizer) exchangeToken(ctx context.Context, tok string) (s
        // server components will accept.
        err = ta.ctrl.setup()
        if err != nil {
-               return "", fmt.Errorf("error setting up OpenID Connect provider: %s", err)
+               return fmt.Errorf("error setting up OpenID Connect provider: %s", err)
        }
        oauth2Token := &oauth2.Token{
                AccessToken: tok,
@@ -452,35 +452,37 @@ func (ta *oidcTokenAuthorizer) exchangeToken(ctx context.Context, tok string) (s
        userinfo, err := ta.ctrl.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
        if err != nil {
                ta.cache.Add(tok, time.Now().Add(tokenCacheNegativeTTL))
-               return tok, nil
+               return nil
        }
-       ctxlog.FromContext(ctx).WithField("userinfo", userinfo).Debug("(*oidcTokenAuthorizer)exchangeToken: got userinfo")
+       ctxlog.FromContext(ctx).WithField("userinfo", userinfo).Debug("(*oidcTokenAuthorizer)registerToken: got userinfo")
        authinfo, err := ta.ctrl.getAuthInfo(ctx, oauth2Token, userinfo)
        if err != nil {
-               return "", err
+               return err
        }
 
        var aca arvados.APIClientAuthorization
        if updating {
                _, err = tx.ExecContext(ctx, `update api_client_authorizations set expires_at=$1 where api_token=$2`, time.Now().Add(tokenCacheTTL+time.Minute), hmac)
                if err != nil {
-                       return "", fmt.Errorf("error adding OIDC access token to database: %w", err)
+                       return fmt.Errorf("error adding OIDC access token to database: %w", err)
                }
+               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)
                if err != nil {
-                       return "", err
+                       return err
                }
                _, err = tx.ExecContext(ctx, `update api_client_authorizations set api_token=$1 where uuid=$2`, hmac, aca.UUID)
                if err != nil {
-                       return "", fmt.Errorf("error adding OIDC access token to database: %w", err)
+                       return fmt.Errorf("error adding OIDC access token to database: %w", err)
                }
                aca.APIToken = hmac
+               ctxlog.FromContext(ctx).WithFields(logrus.Fields{"UUID": aca.UUID, "HMAC": hmac}).Debug("(*oidcTokenAuthorizer)registerToken: inserted api_client_authorizations row")
        }
        err = tx.Commit()
        if err != nil {
-               return "", err
+               return err
        }
        ta.cache.Add(tok, aca)
-       return aca.TokenV2(), nil
+       return nil
 }
index acdc4858118fcb4c3fd5be1a1a65208ed72ff530..be4e8bb0b6a1f11e02f74739b7832bc2013e6869 100644 (file)
@@ -43,7 +43,7 @@ class ArvadosApiToken
     auth = nil
     [params["api_token"],
      params["oauth_token"],
-     env["HTTP_AUTHORIZATION"].andand.match(/(OAuth2|Bearer) ([-\/a-zA-Z0-9]+)/).andand[2],
+     env["HTTP_AUTHORIZATION"].andand.match(/(OAuth2|Bearer) ([!-~]+)/).andand[2],
      *reader_tokens,
     ].each do |supplied|
       next if !supplied
index 868405f043a6fcd25adfa437f8ca17ecc909822c..6a34ed95522f419f88303cc5de3190e833f2d99e 100644 (file)
@@ -186,17 +186,28 @@ class ApiClientAuthorization < ArvadosModel
       end
 
     else
-      # token is not a 'v2' token
+      # token is not a 'v2' token. It could be just the secret part
+      # ("v1 token") -- or it could be an OpenIDConnect access token,
+      # in which case either (a) the controller will have inserted a
+      # row with api_token = hmac(systemroottoken,oidctoken) before
+      # forwarding it, or (b) we'll have done that ourselves, or (c)
+      # we'll need to ask LoginCluster to validate it for us below,
+      # and then insert a local row for a faster lookup next time.
+      hmac = OpenSSL::HMAC.hexdigest('sha256', Rails.configuration.SystemRootToken, token)
       auth = ApiClientAuthorization.
                includes(:user, :api_client).
-               where('api_token=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', token).
+               where('api_token in (?, ?) and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', token, hmac).
                first
       if auth && auth.user
         return auth
-      elsif Rails.configuration.Login.LoginCluster && Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
+      elsif !Rails.configuration.Login.LoginCluster.blank? && Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
         # An unrecognized non-v2 token might be an OIDC Access Token
-        # that can be verified by our login cluster in the code below.
+        # that can be verified by our login cluster in the code
+        # below. If so, we'll stuff the database with hmac instead of
+        # the real OIDC token.
         upstream_cluster_id = Rails.configuration.Login.LoginCluster
+        token_uuid = generate_uuid
+        secret = hmac
       else
         return nil
       end