16159: Validates token uuid & secret before expiring.
[arvados.git] / lib / controller / localdb / logout.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "context"
9         "database/sql"
10         "errors"
11         "fmt"
12         "strings"
13
14         "git.arvados.org/arvados.git/lib/ctrlctx"
15         "git.arvados.org/arvados.git/sdk/go/auth"
16         "git.arvados.org/arvados.git/sdk/go/ctxlog"
17 )
18
19 func (conn *Conn) expireAPIClientAuthorization(ctx context.Context) error {
20         creds, ok := auth.FromContext(ctx)
21         if !ok {
22                 return errors.New("credentials not found from context")
23         }
24
25         if len(creds.Tokens) == 0 {
26                 // Old client may not have provided the token to expire
27                 return nil
28         }
29
30         tx, err := ctrlctx.CurrentTx(ctx)
31         if err != nil {
32                 return err
33         }
34
35         token := creds.Tokens[0]
36         tokenSecret := token
37         var tokenUuid string
38         if strings.HasPrefix(token, "v2/") {
39                 tokenParts := strings.Split(token, "/")
40                 if len(tokenParts) >= 3 {
41                         tokenUuid = tokenParts[1]
42                         tokenSecret = tokenParts[2]
43                 }
44         }
45
46         var retrievedUuid string
47         err = tx.QueryRowContext(ctx, `SELECT uuid FROM api_client_authorizations WHERE api_token=$1 AND (expires_at IS NULL OR expires_at > current_timestamp AT TIME ZONE 'UTC') LIMIT 1`, tokenSecret).Scan(&retrievedUuid)
48         if err == sql.ErrNoRows {
49                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): not found in database", token)
50                 return nil
51         } else if err != nil {
52                 ctxlog.FromContext(ctx).WithError(err).Debugf("expireAPIClientAuthorization(%s): database error", token)
53                 return err
54         }
55         if tokenUuid != "" && retrievedUuid != tokenUuid {
56                 // secret part matches, but UUID doesn't -- somewhat surprising
57                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): secret part found, but with different UUID: %s", tokenSecret, retrievedUuid)
58                 return nil
59         }
60
61         res, err := tx.ExecContext(ctx, "UPDATE api_client_authorizations SET expires_at=current_timestamp AT TIME ZONE 'UTC' WHERE (expires_at IS NULL OR expires_at > current_timestamp AT TIME ZONE 'UTC' AND api_token=$1)", tokenSecret)
62         if err != nil {
63                 return err
64         }
65
66         rows, err := res.RowsAffected()
67         if err != nil {
68                 return err
69         }
70         if rows == 0 {
71                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): no rows were updated", tokenSecret)
72                 return fmt.Errorf("couldn't expire provided token")
73         } else if rows > 1 {
74                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): multiple (%d) rows updated", tokenSecret, rows)
75         } else {
76                 ctxlog.FromContext(ctx).Debugf("expireAPIClientAuthorization(%s): ok", tokenSecret)
77         }
78
79         return nil
80 }