mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
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:
@ -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.",
|
||||
|
Reference in New Issue
Block a user