Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/public-api-server/pkg/auth/personal_access_token.go
2500 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package auth
6
7
import (
8
"crypto/rand"
9
"crypto/sha256"
10
"crypto/subtle"
11
"encoding/base64"
12
"encoding/hex"
13
"errors"
14
"fmt"
15
"math/big"
16
"strings"
17
)
18
19
const PersonalAccessTokenPrefix = "gitpod_pat_"
20
21
// PersonalAccessToken token is an Access Token for individuals. Any action taken with this token will act on behalf of the token creator.
22
// The PersonalAccessToken, in string form, takes the following shape: gitpod_pat_<signature>.<value>
23
// E.g. gitpod_pat_ko8KC1tJ-GkqIwqNliwF4tBUk2Jd5nEe9qOWqYfobtY.6ZDQVanpaTKj9hQuji0thCe8KFCcmEDGpsaTkSSb
24
type PersonalAccessToken struct {
25
// prefix is the human readable prefix for the token used to identify which type of token it is,
26
// but also for code-scanning of leaked credentials.
27
// e.g. `gitpod_pat_`
28
prefix string
29
30
// value is the secret value of the token
31
value string
32
33
// signature is the generated signature of the value
34
// signature is used to validate the personal access token before using it
35
// signature is Base 64 URL Encoded, without padding
36
signature string
37
}
38
39
func (t *PersonalAccessToken) String() string {
40
return fmt.Sprintf("%s%s.%s", t.prefix, t.signature, t.value)
41
}
42
43
func (t *PersonalAccessToken) Value() string {
44
return t.value
45
}
46
47
// ValueHash computes the SHA256 hash of the token value
48
func (t *PersonalAccessToken) ValueHash() string {
49
hashed := sha256.Sum256([]byte(t.value))
50
return hex.EncodeToString(hashed[:])
51
}
52
53
func GeneratePersonalAccessToken(signer Signer) (PersonalAccessToken, error) {
54
if signer == nil {
55
return PersonalAccessToken{}, errors.New("no personal access token signer available")
56
}
57
58
value, err := generateTokenValue(40)
59
if err != nil {
60
return PersonalAccessToken{}, fmt.Errorf("failed to generate personal access token value: %w", err)
61
}
62
63
signature, err := signer.Sign([]byte(value))
64
if err != nil {
65
return PersonalAccessToken{}, fmt.Errorf("failed to sign personal access token value: %w", err)
66
}
67
68
return PersonalAccessToken{
69
prefix: PersonalAccessTokenPrefix,
70
value: value,
71
// We use base64.RawURLEncoding because we do not want padding in the token in the form of '=' signs
72
signature: base64.RawURLEncoding.EncodeToString(signature),
73
}, nil
74
}
75
76
func ParsePersonalAccessToken(token string, signer Signer) (PersonalAccessToken, error) {
77
if token == "" {
78
return PersonalAccessToken{}, errors.New("empty personal access")
79
}
80
// Assume we start with the following token: gitpod_pat_ko8KC1tJ-GkqIwqNliwF4tBUk2Jd5nEe9qOWqYfobtY.6ZDQVanpaTKj9hQuji0thCe8KFCcmEDGpsaTkSSb
81
// First, we identify if the token contains the required prefix
82
if !strings.HasPrefix(token, PersonalAccessTokenPrefix) {
83
return PersonalAccessToken{}, fmt.Errorf("personal access token does not have %s prefix", PersonalAccessTokenPrefix)
84
}
85
86
// Remove the gitpod_pat_ prefix
87
token = strings.TrimPrefix(token, PersonalAccessTokenPrefix)
88
89
// We now have the token in the following form:
90
// ko8KC1tJ-GkqIwqNliwF4tBUk2Jd5nEe9qOWqYfobtY.6ZDQVanpaTKj9hQuji0thCe8KFCcmEDGpsaTkSSb
91
// Break it into <signature>.<value>
92
parts := strings.SplitN(token, ".", 2)
93
if len(parts) != 2 {
94
return PersonalAccessToken{}, errors.New("failed to break personal access token into signature and value")
95
}
96
97
// Sanity check the extracted values
98
signature, value := parts[0], parts[1]
99
if signature == "" {
100
return PersonalAccessToken{}, errors.New("personal access token has empty signature")
101
}
102
if value == "" {
103
return PersonalAccessToken{}, errors.New("personal access token has empty value")
104
}
105
106
// We must validate the signature before we proceed further.
107
signatureForValue, err := signer.Sign([]byte(value))
108
if err != nil {
109
return PersonalAccessToken{}, fmt.Errorf("failed to compute signature of personal access token value: %w", err)
110
}
111
112
// The signature we receive is Base64 encoded, we also encode the signature for value we've just generated.
113
encodedSignatureForValue := base64.RawURLEncoding.EncodeToString(signatureForValue)
114
115
// Perform a cryptographically safe comparison between the signature, and the value we've just signed
116
if subtle.ConstantTimeCompare([]byte(signature), []byte(encodedSignatureForValue)) != 1 {
117
return PersonalAccessToken{}, errors.New("personal access token signature does not match token value")
118
}
119
120
return PersonalAccessToken{
121
prefix: PersonalAccessTokenPrefix,
122
value: value,
123
signature: signature,
124
}, nil
125
}
126
127
func generateTokenValue(size int) (string, error) {
128
if size <= 0 {
129
return "", errors.New("token size must be greater than 0")
130
}
131
132
// letters represent the resulting character-set of the token
133
// we use only upper/lower alphanumberic to ensure the token is
134
// * easy to select by double-clicking it
135
// * URL safe
136
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
137
ret := make([]byte, size)
138
for i := 0; i < size; i++ {
139
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
140
if err != nil {
141
return "", err
142
}
143
ret[i] = letters[num.Int64()]
144
}
145
146
return string(ret), nil
147
}
148
149