-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauthenticator.go
More file actions
179 lines (154 loc) · 4.6 KB
/
authenticator.go
File metadata and controls
179 lines (154 loc) · 4.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package main
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
_ "github.com/lib/pq"
)
type AuthMethod string
const (
AuthPassword AuthMethod = "password"
AuthPat AuthMethod = "pat"
AuthJwt AuthMethod = "jwt"
)
type authenticator struct {
// use password authentication, this falls through to the native scram-sha256 auth
AuthMethod AuthMethod
ApiUrl string
}
type AuthZRequest struct {
Role string `json:"role"`
Rhost string `json:"rhost"`
}
type UserPermissionSet struct {
UserId string `json:"user_id"`
Role UserRole `json:"user_role"`
}
type UserRole struct {
Role string `json:"role"`
}
/* discoverAuthenticator uses the auth token to determine which authentication mechanism to use */
func discoverAuthenticator(ctx context.Context, config *config, token string) (*authenticator, error) {
if looksLikePAT(token) {
return &authenticator{
ApiUrl: config.AuthAPIURL,
AuthMethod: AuthPat,
}, nil
}
if looksLikeJWT(token) {
return &authenticator{
ApiUrl: config.AuthAPIURL,
AuthMethod: AuthJwt,
}, nil
}
return &authenticator{
AuthMethod: AuthPassword,
}, nil
}
// Authenticate authenticates a user with the provided token.
func (a *authenticator) Authenticate(ctx context.Context, user, token string) error {
if a.AuthMethod == AuthPassword {
return authPassword(ctx, user, token)
}
// AuthPat and AuthJwt use the same API
return authApi(ctx, a.ApiUrl, user, token)
}
// looksLikePAT simply checks if a supplied token has the supabase PAT prefix
// sbp_ has length 44 and sbp_oauth is 50
func looksLikePAT(token string) bool {
return len(token) >= 44 && (token[:4] == "sbp_" || token[:13] == "supabase_pat_")
}
// looksLikeJWT checks if a token has the format of a JWT. It does not validate the JWT
func looksLikeJWT(token string) bool {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return false
}
hasPrefix := func(s string) bool {
return len(s) >= 3 && s[:3] == "eyJ"
}
return hasPrefix(parts[0]) && hasPrefix(parts[1])
}
/* authPassword will attempt to auth to the local postgres database */
func authPassword(ctx context.Context, username, password string) error {
connStr := fmt.Sprintf("user=%s password=%s dbname=authdbsupabase sslmode=disable host=127.0.0.1", username, password)
db, err := sql.Open("postgres", connStr)
if err != nil {
return err
}
defer db.Close()
err = db.Ping()
if err != nil {
// valid username + password and permitted to login
if strings.Contains(err.Error(), "database \"authdbsupabase\" does not exist") {
return nil
}
return err
}
return nil
}
func authApi(ctx context.Context, apiUrl, username, token string) error {
// make an API request to check if the user is authorized to login
// uses the incoming token to authenticate against the API
// giving the guarantee that the user token is still valid and permitted
// to interact with the project
if rhost, ok := ctx.Value(rhostKey).(string); !ok {
return fmt.Errorf("context does not have rhost")
} else {
jsonData, err := json.Marshal(&AuthZRequest{username, rhost})
if err != nil {
panic(err)
}
client := &http.Client{}
req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
// set auth for API server, only bearer support for now
req.Header.Add("Authorization", "Bearer "+token)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// user has no authorization setup if a 406 error is returned
if resp.StatusCode == http.StatusNotAcceptable {
return fmt.Errorf("user not authorized for JIT access to database")
}
if resp.StatusCode == http.StatusForbidden {
return fmt.Errorf("user not authorized due to restriction")
}
// something else went wrong
return fmt.Errorf("failed with status: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var perms UserPermissionSet
if err := json.Unmarshal(body, &perms); err != nil {
return err
}
// validate the user's permission
return isPermitted(ctx, username, perms)
}
}
func isPermitted(ctx context.Context, username string, perms UserPermissionSet) error {
if username == "" {
return fmt.Errorf("empty username")
}
// not strictly required anymore,
// but double check the response from the server is what we were expecting
if perms.Role.Role == username {
return nil
}
return fmt.Errorf("not permitted to assume %s", username)
}