feat: enable key rotation (#15066)

This PR contains the remaining logic necessary to hook up key rotation
to the product.
This commit is contained in:
Jon Ayers
2024-10-25 17:14:35 +01:00
committed by GitHub
parent ccfffc6911
commit cd890aa3a0
54 changed files with 1412 additions and 1129 deletions

View File

@ -15,7 +15,8 @@ import (
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt/v4"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/google/go-github/v43/github"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
@ -23,6 +24,9 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/cryptokeys"
"github.com/coder/coder/v2/coderd/idpsync"
"github.com/coder/coder/v2/coderd/jwtutils"
"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/audit"
@ -32,7 +36,6 @@ import (
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/idpsync"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/promoauth"
"github.com/coder/coder/v2/coderd/rbac"
@ -49,7 +52,7 @@ const (
)
type OAuthConvertStateClaims struct {
jwt.RegisteredClaims
jwtutils.RegisteredClaims
UserID uuid.UUID `json:"user_id"`
State string `json:"state"`
@ -57,6 +60,10 @@ type OAuthConvertStateClaims struct {
ToLoginType codersdk.LoginType `json:"to_login_type"`
}
func (o *OAuthConvertStateClaims) Validate(e jwt.Expected) error {
return o.RegisteredClaims.Validate(e)
}
// postConvertLoginType replies with an oauth state token capable of converting
// the user to an oauth user.
//
@ -149,11 +156,11 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) {
// Eg: Developers with more than 1 deployment.
now := time.Now()
claims := &OAuthConvertStateClaims{
RegisteredClaims: jwt.RegisteredClaims{
RegisteredClaims: jwtutils.RegisteredClaims{
Issuer: api.DeploymentID,
Subject: stateString,
Audience: []string{user.ID.String()},
ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 5)),
Expiry: jwt.NewNumericDate(now.Add(time.Minute * 5)),
NotBefore: jwt.NewNumericDate(now.Add(time.Second * -1)),
IssuedAt: jwt.NewNumericDate(now),
ID: uuid.NewString(),
@ -164,9 +171,7 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) {
ToLoginType: req.ToType,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
// Key must be a byte slice, not an array. So make sure to include the [:]
tokenString, err := token.SignedString(api.OAuthSigningKey[:])
token, err := jwtutils.Sign(ctx, api.OIDCConvertKeyCache, claims)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error signing state jwt.",
@ -176,8 +181,8 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) {
}
aReq.New = database.AuditOAuthConvertState{
CreatedAt: claims.IssuedAt.Time,
ExpiresAt: claims.ExpiresAt.Time,
CreatedAt: claims.IssuedAt.Time(),
ExpiresAt: claims.Expiry.Time(),
FromLoginType: database.LoginType(claims.FromLoginType),
ToLoginType: database.LoginType(claims.ToLoginType),
UserID: claims.UserID,
@ -186,8 +191,8 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) {
http.SetCookie(rw, &http.Cookie{
Name: OAuthConvertCookieValue,
Path: "/",
Value: tokenString,
Expires: claims.ExpiresAt.Time,
Value: token,
Expires: claims.Expiry.Time(),
Secure: api.SecureAuthCookie,
HttpOnly: true,
// Must be SameSite to work on the redirected auth flow from the
@ -196,7 +201,7 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) {
})
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.OAuthConversionResponse{
StateString: stateString,
ExpiresAt: claims.ExpiresAt.Time,
ExpiresAt: claims.Expiry.Time(),
ToType: claims.ToLoginType,
UserID: claims.UserID,
})
@ -1677,10 +1682,9 @@ func (api *API) convertUserToOauth(ctx context.Context, r *http.Request, db data
}
}
var claims OAuthConvertStateClaims
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(_ *jwt.Token) (interface{}, error) {
return api.OAuthSigningKey[:], nil
})
if xerrors.Is(err, jwt.ErrSignatureInvalid) || !token.Valid {
err = jwtutils.Verify(ctx, api.OIDCConvertKeyCache, jwtCookie.Value, &claims)
if xerrors.Is(err, cryptokeys.ErrKeyNotFound) || xerrors.Is(err, cryptokeys.ErrKeyInvalid) || xerrors.Is(err, jose.ErrCryptoFailure) || xerrors.Is(err, jwtutils.ErrMissingKeyID) {
// These errors are probably because the user is mixing 2 coder deployments.
return database.User{}, idpsync.HTTPError{
Code: http.StatusBadRequest,
@ -1709,7 +1713,7 @@ func (api *API) convertUserToOauth(ctx context.Context, r *http.Request, db data
oauthConvertAudit.UserID = claims.UserID
oauthConvertAudit.Old = user
if claims.RegisteredClaims.Issuer != api.DeploymentID {
if claims.Issuer != api.DeploymentID {
return database.User{}, idpsync.HTTPError{
Code: http.StatusForbidden,
Msg: "Request to convert login type failed. Issuer mismatch. Found a cookie from another coder deployment, please try again.",