github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/token.go (about) 1 package vcs 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "encoding/base64" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "net/http" 11 "net/url" 12 "os" 13 "strconv" 14 "time" 15 16 "connectrpc.com/connect" 17 "golang.org/x/oauth2" 18 19 "github.com/grafana/pyroscope/pkg/tenant" 20 ) 21 22 const ( 23 sessionCookieName = "pyroscope_git_session" 24 ) 25 26 // Deprecated: this is the old format for encoded token inside a cookie 27 // Remove after completing https://github.com/grafana/explore-profiles/issues/187 28 type deprecatedGitSessionTokenCookie struct { 29 Metadata string `json:"metadata"` 30 ExpiryTimestamp int64 `json:"expiry"` 31 } 32 33 type gitSessionTokenCookie struct { 34 Token *string `json:"token"` 35 } 36 37 const envVarGithubSessionSecret = "GITHUB_SESSION_SECRET" 38 39 var githubSessionSecret = []byte(os.Getenv(envVarGithubSessionSecret)) 40 41 // derives a per tenant key from the global session secret using sha256 42 func deriveEncryptionKeyForContext(ctx context.Context) ([]byte, error) { 43 tenantID, err := tenant.ExtractTenantIDFromContext(ctx) 44 if err != nil { 45 return nil, err 46 } 47 if len(tenantID) == 0 { 48 return nil, errors.New("tenantID is empty") 49 } 50 51 if len(githubSessionSecret) == 0 { 52 return nil, errors.New(envVarGithubSessionSecret + " is empty") 53 } 54 h := sha256.New() 55 h.Write(githubSessionSecret) 56 h.Write([]byte{':'}) 57 h.Write([]byte(tenantID)) 58 return h.Sum(nil), nil 59 } 60 61 // getStringValueFrom gets a string value from url.Values. It will fail if the 62 // key is missing or the key's value is an empty string. 63 func getStringValueFrom(values url.Values, key string) (string, error) { 64 value := values.Get(key) 65 if value == "" { 66 return "", fmt.Errorf("missing key: %s", key) 67 } 68 return value, nil 69 } 70 71 // getDurationValueFrom gets a duration value from url.Values. It will fail if 72 // the key is missing, the key's value is an empty string, or the key's value 73 // cannot be parsed into a duration. 74 func getDurationValueFrom(values url.Values, key string, scalar time.Duration) (time.Duration, error) { 75 if scalar < 1 { 76 return 0, fmt.Errorf("cannot use scalar less than 1") 77 } 78 79 value, err := getStringValueFrom(values, key) 80 if err != nil { 81 return 0, err 82 } 83 84 n, err := strconv.Atoi(value) 85 if err != nil { 86 return 0, fmt.Errorf("failed to parse %s: %w", key, err) 87 } 88 89 return time.Duration(n) * scalar, nil 90 } 91 92 // tokenFromRequest decodes an OAuth token from a request. 93 func tokenFromRequest(ctx context.Context, req connect.AnyRequest) (*oauth2.Token, error) { 94 cookie, err := (&http.Request{Header: req.Header()}).Cookie(sessionCookieName) 95 if err != nil { 96 return nil, fmt.Errorf("failed to read cookie %s: %w", sessionCookieName, err) 97 } 98 99 derivedKey, err := deriveEncryptionKeyForContext(ctx) 100 if err != nil { 101 return nil, err 102 } 103 104 token, err := decodeToken(cookie.Value, derivedKey) 105 if err != nil { 106 return nil, err 107 } 108 return token, nil 109 } 110 111 // Deprecated: encodeTokenInCookie creates a cookie by encrypting then base64 encoding an OAuth token. 112 // In future version, the cookie that this function creates will be no longer sent by the backend. 113 // Instead, backend provides the necessary data so frontend can create its own GitHub session cookie. 114 // Remove after completing https://github.com/grafana/explore-profiles/issues/187 115 func encodeTokenInCookie(token *oauth2.Token, key []byte) (*http.Cookie, error) { 116 encrypted, err := encryptToken(token, key) 117 if err != nil { 118 return nil, err 119 } 120 121 bytes, err := json.Marshal(deprecatedGitSessionTokenCookie{ 122 Metadata: encrypted, 123 ExpiryTimestamp: token.Expiry.UnixMilli(), 124 }) 125 if err != nil { 126 return nil, err 127 } 128 129 encoded := base64.StdEncoding.EncodeToString(bytes) 130 cookie := &http.Cookie{ 131 Name: sessionCookieName, 132 Value: encoded, 133 Expires: time.Now().Add(githubRefreshExpiryDuration), 134 HttpOnly: false, 135 Secure: true, 136 SameSite: http.SameSiteLaxMode, 137 } 138 return cookie, nil 139 } 140 141 // decodeToken base64 decodes and decrypts a OAuth token. 142 func decodeToken(value string, key []byte) (*oauth2.Token, error) { 143 var token *oauth2.Token 144 145 decoded, err := base64.StdEncoding.DecodeString(value) 146 if err != nil { 147 return nil, err 148 } 149 150 sessionToken := gitSessionTokenCookie{} 151 err = json.Unmarshal(decoded, &sessionToken) 152 if err != nil || sessionToken.Token == nil { 153 // This may be a deprecated cookie. Deprecated cookies are base64 encoded deprecatedGitSessionTokenCookie objects. 154 token, innerErr := decodeDeprecatedToken(decoded, key) 155 if innerErr != nil { 156 // Deprecated fallback failed, return the original error if exists. 157 if err != nil { 158 return nil, err 159 } 160 return nil, innerErr 161 } 162 return token, nil 163 } 164 165 token, err = decryptToken(*sessionToken.Token, key) 166 if err != nil { 167 return nil, err 168 } 169 170 return token, nil 171 } 172 173 // Deprecated: decodeDeprecatedToken decrypts a deprecatedGitSessionTokenCookie 174 // In future version, frontend won't send any deprecated cookies. 175 // Remove alongside encodeTokenInCookie, after completing https://github.com/grafana/explore-profiles/issues/187 176 func decodeDeprecatedToken(value []byte, key []byte) (*oauth2.Token, error) { 177 var token *oauth2.Token 178 179 sessionToken := &deprecatedGitSessionTokenCookie{} 180 err := json.Unmarshal(value, sessionToken) 181 if err != nil || sessionToken == nil { 182 return nil, err 183 } 184 185 token, err = decryptToken(sessionToken.Metadata, key) 186 if err != nil { 187 return nil, err 188 } 189 return token, nil 190 }