diff options
Diffstat (limited to 'auth/token.go')
-rw-r--r-- | auth/token.go | 92 |
1 files changed, 83 insertions, 9 deletions
diff --git a/auth/token.go b/auth/token.go index fc13c5bee..e1c196615 100644 --- a/auth/token.go +++ b/auth/token.go @@ -1,8 +1,14 @@ package gitalyauth import ( + "crypto/hmac" + "crypto/sha256" "crypto/subtle" "encoding/base64" + "encoding/hex" + "strconv" + "strings" + "time" "github.com/grpc-ecosystem/go-grpc-middleware/auth" "golang.org/x/net/context" @@ -10,36 +16,104 @@ import ( "google.golang.org/grpc/status" ) +const ( + timestampThreshold = 30 * time.Second +) + var ( errUnauthenticated = status.Errorf(codes.Unauthenticated, "authentication required") errDenied = status.Errorf(codes.PermissionDenied, "permission denied") ) +// AuthInfo contains the authentication information coming from a request +type AuthInfo struct { + Version string + SignedMessage []byte + Message string +} + // CheckToken checks the 'authentication' header of incoming gRPC // metadata in ctx. It returns nil if and only if the token matches // secret. -func CheckToken(ctx context.Context, secret string) error { +func CheckToken(ctx context.Context, secret string, targetTime time.Time) error { if len(secret) == 0 { panic("CheckToken: secret may not be empty") } - encodedToken, err := grpc_auth.AuthFromMD(ctx, "bearer") + authInfo, err := ExtractAuthInfo(ctx) if err != nil { return errUnauthenticated } - token, err := base64.StdEncoding.DecodeString(encodedToken) - if err != nil { - return errUnauthenticated - } + switch authInfo.Version { + case "v1": + decodedToken, err := base64.StdEncoding.DecodeString(authInfo.Message) + if err != nil { + return errUnauthenticated + } - if !tokensEqual(token, []byte(secret)) { - return errDenied + if tokensEqual(decodedToken, []byte(secret)) { + return nil + } + case "v2": + if hmacInfoValid(authInfo.Message, authInfo.SignedMessage, []byte(secret), targetTime, timestampThreshold) { + return nil + } } - return nil + return errDenied } func tokensEqual(tok1, tok2 []byte) bool { return subtle.ConstantTimeCompare(tok1, tok2) == 1 } + +// ExtractAuthInfo returns an `AuthInfo` with the data extracted from `ctx` +func ExtractAuthInfo(ctx context.Context) (*AuthInfo, error) { + token, err := grpc_auth.AuthFromMD(ctx, "bearer") + + if err != nil { + return nil, err + } + + split := strings.SplitN(string(token), ".", 3) + + // v1 is base64-encoded using base64.StdEncoding, which cannot contain a ".". + // A v1 token cannot slip through here. + if len(split) != 3 { + return &AuthInfo{Version: "v1", Message: token}, nil + } + + version, sig, msg := split[0], split[1], split[2] + decodedSig, err := hex.DecodeString(sig) + if err != nil { + return nil, err + } + + return &AuthInfo{Version: version, SignedMessage: decodedSig, Message: msg}, nil +} + +func hmacInfoValid(message string, signedMessage, secret []byte, targetTime time.Time, timestampThreshold time.Duration) bool { + expectedHMAC := hmacSign(secret, message) + if !hmac.Equal(signedMessage, expectedHMAC) { + return false + } + + timestamp, err := strconv.ParseInt(message, 10, 64) + if err != nil { + return false + } + + issuedAt := time.Unix(timestamp, 0) + lowerBound := targetTime.Add(-timestampThreshold) + upperBound := targetTime.Add(timestampThreshold) + + return issuedAt.After(lowerBound) && issuedAt.Before(upperBound) +} + +func hmacSign(secret []byte, message string) []byte { + mac := hmac.New(sha256.New, secret) + mac.Write([]byte(message)) + + return mac.Sum(nil) +} |