grafana/apps/provisioning/pkg/connection/github/token.go

90 lines
2.8 KiB
Go

package github
import (
"encoding/base64"
"fmt"
"time"
"github.com/golang-jwt/jwt/v4"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
const (
// JWTExpirationMinutes is the token expiration time for GitHub App JWT tokens
// Based on https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
// The expiration time muse max 10 minutes.
JWTExpirationMinutes = 10
)
// GenerateJWTToken creates a GitHub App JWT token from appID and base64-encoded private key.
// The private key should be base64-encoded PEM format.
// Returns the signed JWT token string.
// related to how Github wants their token to be built.
func GenerateJWTToken(appID string, privateKey common.RawSecureValue) (common.RawSecureValue, error) {
// Decode base64-encoded private key
privateKeyPEM, err := base64.StdEncoding.DecodeString(string(privateKey))
if err != nil {
return "", fmt.Errorf("failed to decode base64 private key: %w", err)
}
// Parse the private key
key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEM)
if err != nil {
return "", fmt.Errorf("failed to parse private key: %w", err)
}
// Create the JWT token
now := time.Now()
claims := jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(JWTExpirationMinutes) * time.Minute)),
Issuer: appID,
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signedToken, err := token.SignedString(key)
if err != nil {
return "", fmt.Errorf("failed to sign JWT token: %w", err)
}
return common.RawSecureValue(signedToken), nil
}
func parseJWTToken(token, privateKey common.RawSecureValue) (*jwt.RegisteredClaims, error) {
privateKeyPEM, err := base64.StdEncoding.DecodeString(string(privateKey))
if err != nil {
return nil, fmt.Errorf("failed to decode base64 private key: %w", err)
}
key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
parser := jwt.NewParser(
jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
jwt.WithoutClaimsValidation(),
)
parsedToken, err := parser.ParseWithClaims(string(token), &jwt.RegisteredClaims{}, func(_ *jwt.Token) (any, error) {
return &key.PublicKey, nil
})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
claims, ok := parsedToken.Claims.(*jwt.RegisteredClaims)
if !ok {
return nil, fmt.Errorf("unexpected token claims")
}
return claims, nil
}
func getIssuingAndExpirationTimeFromToken(token, privateKey common.RawSecureValue) (time.Time, time.Time, error) {
claims, err := parseJWTToken(token, privateKey)
if err != nil {
return time.Time{}, time.Time{}, fmt.Errorf("failed to parse token: %w", err)
}
return claims.IssuedAt.Time, claims.ExpiresAt.Time, nil
}