Path: blob/main/components/public-api-server/pkg/auth/personal_access_token.go
2500 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package auth56import (7"crypto/rand"8"crypto/sha256"9"crypto/subtle"10"encoding/base64"11"encoding/hex"12"errors"13"fmt"14"math/big"15"strings"16)1718const PersonalAccessTokenPrefix = "gitpod_pat_"1920// PersonalAccessToken token is an Access Token for individuals. Any action taken with this token will act on behalf of the token creator.21// The PersonalAccessToken, in string form, takes the following shape: gitpod_pat_<signature>.<value>22// E.g. gitpod_pat_ko8KC1tJ-GkqIwqNliwF4tBUk2Jd5nEe9qOWqYfobtY.6ZDQVanpaTKj9hQuji0thCe8KFCcmEDGpsaTkSSb23type PersonalAccessToken struct {24// prefix is the human readable prefix for the token used to identify which type of token it is,25// but also for code-scanning of leaked credentials.26// e.g. `gitpod_pat_`27prefix string2829// value is the secret value of the token30value string3132// signature is the generated signature of the value33// signature is used to validate the personal access token before using it34// signature is Base 64 URL Encoded, without padding35signature string36}3738func (t *PersonalAccessToken) String() string {39return fmt.Sprintf("%s%s.%s", t.prefix, t.signature, t.value)40}4142func (t *PersonalAccessToken) Value() string {43return t.value44}4546// ValueHash computes the SHA256 hash of the token value47func (t *PersonalAccessToken) ValueHash() string {48hashed := sha256.Sum256([]byte(t.value))49return hex.EncodeToString(hashed[:])50}5152func GeneratePersonalAccessToken(signer Signer) (PersonalAccessToken, error) {53if signer == nil {54return PersonalAccessToken{}, errors.New("no personal access token signer available")55}5657value, err := generateTokenValue(40)58if err != nil {59return PersonalAccessToken{}, fmt.Errorf("failed to generate personal access token value: %w", err)60}6162signature, err := signer.Sign([]byte(value))63if err != nil {64return PersonalAccessToken{}, fmt.Errorf("failed to sign personal access token value: %w", err)65}6667return PersonalAccessToken{68prefix: PersonalAccessTokenPrefix,69value: value,70// We use base64.RawURLEncoding because we do not want padding in the token in the form of '=' signs71signature: base64.RawURLEncoding.EncodeToString(signature),72}, nil73}7475func ParsePersonalAccessToken(token string, signer Signer) (PersonalAccessToken, error) {76if token == "" {77return PersonalAccessToken{}, errors.New("empty personal access")78}79// Assume we start with the following token: gitpod_pat_ko8KC1tJ-GkqIwqNliwF4tBUk2Jd5nEe9qOWqYfobtY.6ZDQVanpaTKj9hQuji0thCe8KFCcmEDGpsaTkSSb80// First, we identify if the token contains the required prefix81if !strings.HasPrefix(token, PersonalAccessTokenPrefix) {82return PersonalAccessToken{}, fmt.Errorf("personal access token does not have %s prefix", PersonalAccessTokenPrefix)83}8485// Remove the gitpod_pat_ prefix86token = strings.TrimPrefix(token, PersonalAccessTokenPrefix)8788// We now have the token in the following form:89// ko8KC1tJ-GkqIwqNliwF4tBUk2Jd5nEe9qOWqYfobtY.6ZDQVanpaTKj9hQuji0thCe8KFCcmEDGpsaTkSSb90// Break it into <signature>.<value>91parts := strings.SplitN(token, ".", 2)92if len(parts) != 2 {93return PersonalAccessToken{}, errors.New("failed to break personal access token into signature and value")94}9596// Sanity check the extracted values97signature, value := parts[0], parts[1]98if signature == "" {99return PersonalAccessToken{}, errors.New("personal access token has empty signature")100}101if value == "" {102return PersonalAccessToken{}, errors.New("personal access token has empty value")103}104105// We must validate the signature before we proceed further.106signatureForValue, err := signer.Sign([]byte(value))107if err != nil {108return PersonalAccessToken{}, fmt.Errorf("failed to compute signature of personal access token value: %w", err)109}110111// The signature we receive is Base64 encoded, we also encode the signature for value we've just generated.112encodedSignatureForValue := base64.RawURLEncoding.EncodeToString(signatureForValue)113114// Perform a cryptographically safe comparison between the signature, and the value we've just signed115if subtle.ConstantTimeCompare([]byte(signature), []byte(encodedSignatureForValue)) != 1 {116return PersonalAccessToken{}, errors.New("personal access token signature does not match token value")117}118119return PersonalAccessToken{120prefix: PersonalAccessTokenPrefix,121value: value,122signature: signature,123}, nil124}125126func generateTokenValue(size int) (string, error) {127if size <= 0 {128return "", errors.New("token size must be greater than 0")129}130131// letters represent the resulting character-set of the token132// we use only upper/lower alphanumberic to ensure the token is133// * easy to select by double-clicking it134// * URL safe135const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"136ret := make([]byte, size)137for i := 0; i < size; i++ {138num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))139if err != nil {140return "", err141}142ret[i] = letters[num.Int64()]143}144145return string(ret), nil146}147148149