e1a0367b03
Most of the changes in this commit are because of these fosite PRs which changed behavior and/or APIs in fosite: - https://github.com/ory/fosite/pull/667 - https://github.com/ory/fosite/pull/679 (from me!) - https://github.com/ory/fosite/pull/675 - https://github.com/ory/fosite/pull/688 Due to the changes in fosite PR #688, we need to bump our storage version for anything which stores the DefaultSession struct as JSON.
482 lines
21 KiB
Go
482 lines
21 KiB
Go
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package oidc contains common OIDC functionality needed by Pinniped.
|
|
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/felixge/httpsnoop"
|
|
"github.com/ory/fosite"
|
|
"github.com/ory/fosite/compose"
|
|
errorsx "github.com/pkg/errors"
|
|
|
|
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
|
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
|
"go.pinniped.dev/internal/httputil/httperr"
|
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
|
"go.pinniped.dev/internal/oidc/jwks"
|
|
"go.pinniped.dev/internal/oidc/provider"
|
|
"go.pinniped.dev/internal/oidc/provider/formposthtml"
|
|
"go.pinniped.dev/internal/plog"
|
|
"go.pinniped.dev/internal/psession"
|
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
|
)
|
|
|
|
const (
|
|
WellKnownEndpointPath = "/.well-known/openid-configuration"
|
|
AuthorizationEndpointPath = "/oauth2/authorize"
|
|
TokenEndpointPath = "/oauth2/token" //nolint:gosec // ignore lint warning that this is a credential
|
|
CallbackEndpointPath = "/callback"
|
|
JWKSEndpointPath = "/jwks.json"
|
|
PinnipedIDPsPathV1Alpha1 = "/v1alpha1/pinniped_identity_providers"
|
|
PinnipedLoginPath = "/login"
|
|
)
|
|
|
|
const (
|
|
// UpstreamStateParamFormatVersion exists just in case we need to make a breaking change to the format of the
|
|
// upstream state param, we are including a format version number. This gives the opportunity for a future version
|
|
// of Pinniped to have the consumer of this format decide to reject versions that it doesn't understand.
|
|
//
|
|
// Version 1 was the original version.
|
|
// Version 2 added the UpstreamType field to the UpstreamStateParamData struct.
|
|
UpstreamStateParamFormatVersion = "2"
|
|
|
|
// UpstreamStateParamEncodingName is the `name` passed to the encoder for encoding the upstream state param value.
|
|
// This name is short because it will be encoded into the upstream state param value, and we're trying to keep that
|
|
// small.
|
|
UpstreamStateParamEncodingName = "s"
|
|
|
|
// CSRFCookieName is the name of the browser cookie which shall hold our CSRF value.
|
|
// The `__Host` prefix has a special meaning. See:
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Cookie_prefixes.
|
|
CSRFCookieName = "__Host-pinniped-csrf"
|
|
|
|
// CSRFCookieEncodingName is the `name` passed to the encoder for encoding and decoding the CSRF
|
|
// cookie contents.
|
|
CSRFCookieEncodingName = "csrf"
|
|
|
|
// CSRFCookieLifespan is the length of time that the CSRF cookie is valid. After this time, the
|
|
// Supervisor's authorization endpoint should give the browser a new CSRF cookie. We set it to
|
|
// a week so that it is unlikely to expire during a login.
|
|
CSRFCookieLifespan = time.Hour * 24 * 7
|
|
)
|
|
|
|
// Encoder is the encoding side of the securecookie.Codec interface.
|
|
type Encoder interface {
|
|
Encode(name string, value interface{}) (string, error)
|
|
}
|
|
|
|
// Decoder is the decoding side of the securecookie.Codec interface.
|
|
type Decoder interface {
|
|
Decode(name, value string, into interface{}) error
|
|
}
|
|
|
|
// Codec is both the encoding and decoding sides of the securecookie.Codec interface. It is
|
|
// interface'd here so that we properly wrap the securecookie dependency.
|
|
type Codec interface {
|
|
Encoder
|
|
Decoder
|
|
}
|
|
|
|
// UpstreamStateParamData is the format of the state parameter that we use when we communicate to an
|
|
// upstream OIDC provider.
|
|
//
|
|
// Keep the JSON to a minimal size because the upstream provider could impose size limitations on
|
|
// the state param.
|
|
type UpstreamStateParamData struct {
|
|
AuthParams string `json:"p"`
|
|
UpstreamName string `json:"u"`
|
|
UpstreamType string `json:"t"`
|
|
Nonce nonce.Nonce `json:"n"`
|
|
CSRFToken csrftoken.CSRFToken `json:"c"`
|
|
PKCECode pkce.Code `json:"k"`
|
|
FormatVersion string `json:"v"`
|
|
}
|
|
|
|
type TimeoutsConfiguration struct {
|
|
// The length of time that our state param that we encrypt and pass to the upstream OIDC IDP should be considered
|
|
// valid. If a state param generated by the authorize endpoint is sent to the callback endpoint after this much
|
|
// time has passed, then the callback endpoint should reject it. This allows us to set a limit on how long
|
|
// the end user has to finish their login with the upstream IDP, including the time that it takes to fumble
|
|
// with password manager and two-factor authenticator apps, and also accounting for taking a coffee break while
|
|
// the browser is sitting at the upstream IDP's login page.
|
|
UpstreamStateParamLifespan time.Duration
|
|
|
|
// How long an authcode issued by the callback endpoint is valid. This determines how much time the end user
|
|
// has to come back to exchange the authcode for tokens at the token endpoint.
|
|
AuthorizeCodeLifespan time.Duration
|
|
|
|
// The lifetime of an downstream access token issued by the token endpoint. Access tokens should generally
|
|
// be fairly short-lived.
|
|
AccessTokenLifespan time.Duration
|
|
|
|
// The lifetime of an downstream ID token issued by the token endpoint. This should generally be the same
|
|
// as the AccessTokenLifespan, or longer if it would be useful for the user's proof of identity to be valid
|
|
// for longer than their proof of authorization.
|
|
IDTokenLifespan time.Duration
|
|
|
|
// The lifetime of an downstream refresh token issued by the token endpoint. This should generally be
|
|
// significantly longer than the access token lifetime, so it can be used to refresh the access token
|
|
// multiple times. Once the refresh token expires, the user's session is over and they will need
|
|
// to start a new authorization request, which will require them to log in again with the upstream IDP
|
|
// in their web browser.
|
|
RefreshTokenLifespan time.Duration
|
|
|
|
// AuthorizationCodeSessionStorageLifetime is the length of time after which an authcode is allowed to be garbage
|
|
// collected from storage. Authcodes are kept in storage after they are redeemed to allow the system to mark the
|
|
// authcode as already used, so it can reject any future uses of the same authcode with special case handling which
|
|
// include revoking the access and refresh tokens associated with the session. Therefore, this should be
|
|
// significantly longer than the AuthorizeCodeLifespan, and there is probably no reason to make it longer than
|
|
// the sum of the AuthorizeCodeLifespan and the RefreshTokenLifespan.
|
|
AuthorizationCodeSessionStorageLifetime time.Duration
|
|
|
|
// PKCESessionStorageLifetime is the length of time after which PKCE data is allowed to be garbage collected from
|
|
// storage. PKCE sessions are closely related to authorization code sessions. After the authcode is successfully
|
|
// redeemed, the PKCE session is explicitly deleted. After the authcode expires, the PKCE session is no longer needed,
|
|
// but it is not explicitly deleted. Therefore, this can be just slightly longer than the AuthorizeCodeLifespan. We'll
|
|
// avoid making it exactly the same as AuthorizeCodeLifespan to avoid any chance of the garbage collector deleting it
|
|
// while it is being used.
|
|
PKCESessionStorageLifetime time.Duration
|
|
|
|
// OIDCSessionStorageLifetime is the length of time after which the OIDC session data related to an authcode
|
|
// is allowed to be garbage collected from storage. Due to a bug in an underlying library, these are not explicitly
|
|
// deleted. Similar to the PKCE session, they are not needed anymore after the corresponding authcode has expired.
|
|
// Therefore, this can be just slightly longer than the AuthorizeCodeLifespan. We'll avoid making it exactly the same
|
|
// as AuthorizeCodeLifespan to avoid any chance of the garbage collector deleting it while it is being used.
|
|
OIDCSessionStorageLifetime time.Duration
|
|
|
|
// AccessTokenSessionStorageLifetime is the length of time after which an access token's session data is allowed
|
|
// to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid
|
|
// or else the refresh flow will not work properly. So this must be longer than RefreshTokenLifespan.
|
|
AccessTokenSessionStorageLifetime time.Duration
|
|
|
|
// RefreshTokenSessionStorageLifetime is the length of time after which a refresh token's session data is allowed
|
|
// to be garbage collected from storage. These must exist in storage for as long as the refresh token is valid.
|
|
// Therefore, this can be just slightly longer than the RefreshTokenLifespan. We'll avoid making it exactly the same
|
|
// as RefreshTokenLifespan to avoid any chance of the garbage collector deleting it while it is being used.
|
|
// If an expired token is still stored when the user tries to refresh it, then they will get a more specific
|
|
// error message telling them that the token is expired, rather than a more generic error that is returned
|
|
// when the token does not exist. If this is desirable, then the RefreshTokenSessionStorageLifetime can be made
|
|
// to be significantly larger than RefreshTokenLifespan, at the cost of slower cleanup.
|
|
RefreshTokenSessionStorageLifetime time.Duration
|
|
}
|
|
|
|
// Get the defaults for the Supervisor server.
|
|
func DefaultOIDCTimeoutsConfiguration() TimeoutsConfiguration {
|
|
accessTokenLifespan := 2 * time.Minute
|
|
authorizationCodeLifespan := 10 * time.Minute
|
|
refreshTokenLifespan := 9 * time.Hour
|
|
|
|
return TimeoutsConfiguration{
|
|
UpstreamStateParamLifespan: 90 * time.Minute,
|
|
AuthorizeCodeLifespan: authorizationCodeLifespan,
|
|
AccessTokenLifespan: accessTokenLifespan,
|
|
IDTokenLifespan: accessTokenLifespan,
|
|
RefreshTokenLifespan: refreshTokenLifespan,
|
|
AuthorizationCodeSessionStorageLifetime: authorizationCodeLifespan + refreshTokenLifespan,
|
|
PKCESessionStorageLifetime: authorizationCodeLifespan + (1 * time.Minute),
|
|
OIDCSessionStorageLifetime: authorizationCodeLifespan + (1 * time.Minute),
|
|
AccessTokenSessionStorageLifetime: refreshTokenLifespan + accessTokenLifespan,
|
|
RefreshTokenSessionStorageLifetime: refreshTokenLifespan + accessTokenLifespan,
|
|
}
|
|
}
|
|
|
|
func FositeOauth2Helper(
|
|
oauthStore interface{},
|
|
issuer string,
|
|
hmacSecretOfLengthAtLeast32Func func() []byte,
|
|
jwksProvider jwks.DynamicJWKSProvider,
|
|
timeoutsConfiguration TimeoutsConfiguration,
|
|
) fosite.OAuth2Provider {
|
|
isRedirectURISecureStrict := func(_ context.Context, uri *url.URL) bool {
|
|
return fosite.IsRedirectURISecureStrict(uri)
|
|
}
|
|
|
|
oauthConfig := &fosite.Config{
|
|
IDTokenIssuer: issuer,
|
|
|
|
AuthorizeCodeLifespan: timeoutsConfiguration.AuthorizeCodeLifespan,
|
|
IDTokenLifespan: timeoutsConfiguration.IDTokenLifespan,
|
|
AccessTokenLifespan: timeoutsConfiguration.AccessTokenLifespan,
|
|
RefreshTokenLifespan: timeoutsConfiguration.RefreshTokenLifespan,
|
|
|
|
ScopeStrategy: fosite.ExactScopeStrategy,
|
|
EnforcePKCE: true,
|
|
|
|
// "offline_access" as per https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
|
RefreshTokenScopes: []string{oidcapi.ScopeOfflineAccess},
|
|
|
|
// The default is to support all prompt values from the spec.
|
|
// See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
AllowedPromptValues: nil,
|
|
|
|
// Use the fosite default to make it more likely that off the shelf OIDC clients can work with the supervisor.
|
|
MinParameterEntropy: fosite.MinParameterEntropy,
|
|
|
|
// do not allow custom scheme redirects, only https and http (on loopback)
|
|
RedirectSecureChecker: isRedirectURISecureStrict,
|
|
|
|
// html template for rendering the authorization response when the request has response_mode=form_post
|
|
FormPostHTMLTemplate: formposthtml.Template(),
|
|
|
|
// defaults to using BCrypt when nil
|
|
ClientSecretsHasher: nil,
|
|
}
|
|
|
|
oAuth2Provider := compose.Compose(
|
|
oauthConfig,
|
|
oauthStore,
|
|
&compose.CommonStrategy{
|
|
// Note that Fosite requires the HMAC secret to be at least 32 bytes.
|
|
CoreStrategy: newDynamicOauth2HMACStrategy(oauthConfig, hmacSecretOfLengthAtLeast32Func),
|
|
OpenIDConnectTokenStrategy: newDynamicOpenIDConnectECDSAStrategy(oauthConfig, jwksProvider),
|
|
},
|
|
compose.OAuth2AuthorizeExplicitFactory,
|
|
compose.OAuth2RefreshTokenGrantFactory,
|
|
compose.OpenIDConnectExplicitFactory,
|
|
compose.OpenIDConnectRefreshFactory,
|
|
compose.OAuth2PKCEFactory,
|
|
TokenExchangeFactory, // handle the "urn:ietf:params:oauth:grant-type:token-exchange" grant type
|
|
)
|
|
|
|
return oAuth2Provider
|
|
}
|
|
|
|
// FositeErrorForLog generates a list of information about the provided Fosite error that can be
|
|
// passed to a plog function (e.g., plog.Info()).
|
|
//
|
|
// Sample usage:
|
|
//
|
|
// err := someFositeLibraryFunction()
|
|
// if err != nil {
|
|
// plog.Info("some error", FositeErrorForLog(err)...)
|
|
// ...
|
|
// }
|
|
func FositeErrorForLog(err error) []interface{} {
|
|
rfc6749Error := fosite.ErrorToRFC6749Error(err)
|
|
keysAndValues := make([]interface{}, 0)
|
|
keysAndValues = append(keysAndValues, "name")
|
|
keysAndValues = append(keysAndValues, rfc6749Error.Error()) // Error() returns the ErrorField
|
|
keysAndValues = append(keysAndValues, "status")
|
|
keysAndValues = append(keysAndValues, rfc6749Error.Status()) // Status() encodes the CodeField as a string
|
|
keysAndValues = append(keysAndValues, "description")
|
|
keysAndValues = append(keysAndValues, rfc6749Error.GetDescription()) // GetDescription() returns the DescriptionField and the HintField
|
|
keysAndValues = append(keysAndValues, "debug")
|
|
keysAndValues = append(keysAndValues, rfc6749Error.Debug()) // Debug() returns the DebugField
|
|
if cause := rfc6749Error.Cause(); cause != nil { // Cause() returns the underlying error, or nil
|
|
keysAndValues = append(keysAndValues, "cause")
|
|
keysAndValues = append(keysAndValues, cause.Error())
|
|
}
|
|
return keysAndValues
|
|
}
|
|
|
|
type UpstreamOIDCIdentityProvidersLister interface {
|
|
GetOIDCIdentityProviders() []provider.UpstreamOIDCIdentityProviderI
|
|
}
|
|
|
|
type UpstreamLDAPIdentityProvidersLister interface {
|
|
GetLDAPIdentityProviders() []provider.UpstreamLDAPIdentityProviderI
|
|
}
|
|
|
|
type UpstreamActiveDirectoryIdentityProviderLister interface {
|
|
GetActiveDirectoryIdentityProviders() []provider.UpstreamLDAPIdentityProviderI
|
|
}
|
|
|
|
type UpstreamIdentityProvidersLister interface {
|
|
UpstreamOIDCIdentityProvidersLister
|
|
UpstreamLDAPIdentityProvidersLister
|
|
UpstreamActiveDirectoryIdentityProviderLister
|
|
}
|
|
|
|
func GrantScopeIfRequested(authorizeRequester fosite.AuthorizeRequester, scopeName string) {
|
|
if ScopeWasRequested(authorizeRequester, scopeName) {
|
|
authorizeRequester.GrantScope(scopeName)
|
|
}
|
|
}
|
|
|
|
func ScopeWasRequested(authorizeRequester fosite.AuthorizeRequester, scopeName string) bool {
|
|
for _, scope := range authorizeRequester.GetRequestedScopes() {
|
|
if scope == scopeName {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func ReadStateParamAndValidateCSRFCookie(r *http.Request, cookieDecoder Decoder, stateDecoder Decoder) (string, *UpstreamStateParamData, error) {
|
|
csrfValue, err := readCSRFCookie(r, cookieDecoder)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
encodedState, decodedState, err := readStateParam(r, stateDecoder)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
err = validateCSRFValue(decodedState, csrfValue)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
return encodedState, decodedState, nil
|
|
}
|
|
|
|
func readCSRFCookie(r *http.Request, cookieDecoder Decoder) (csrftoken.CSRFToken, error) {
|
|
receivedCSRFCookie, err := r.Cookie(CSRFCookieName)
|
|
if err != nil {
|
|
// Error means that the cookie was not found
|
|
return "", httperr.Wrap(http.StatusForbidden, "CSRF cookie is missing", err)
|
|
}
|
|
|
|
var csrfFromCookie csrftoken.CSRFToken
|
|
err = cookieDecoder.Decode(CSRFCookieEncodingName, receivedCSRFCookie.Value, &csrfFromCookie)
|
|
if err != nil {
|
|
return "", httperr.Wrap(http.StatusForbidden, "error reading CSRF cookie", err)
|
|
}
|
|
|
|
return csrfFromCookie, nil
|
|
}
|
|
|
|
func readStateParam(r *http.Request, stateDecoder Decoder) (string, *UpstreamStateParamData, error) {
|
|
encodedState := r.FormValue("state")
|
|
|
|
if encodedState == "" {
|
|
return "", nil, httperr.New(http.StatusBadRequest, "state param not found")
|
|
}
|
|
|
|
var state UpstreamStateParamData
|
|
if err := stateDecoder.Decode(
|
|
UpstreamStateParamEncodingName,
|
|
r.FormValue("state"),
|
|
&state,
|
|
); err != nil {
|
|
return "", nil, httperr.New(http.StatusBadRequest, "error reading state")
|
|
}
|
|
|
|
if state.FormatVersion != UpstreamStateParamFormatVersion {
|
|
return "", nil, httperr.New(http.StatusUnprocessableEntity, "state format version is invalid")
|
|
}
|
|
|
|
return encodedState, &state, nil
|
|
}
|
|
|
|
func validateCSRFValue(state *UpstreamStateParamData, csrfCookieValue csrftoken.CSRFToken) error {
|
|
if subtle.ConstantTimeCompare([]byte(state.CSRFToken), []byte(csrfCookieValue)) != 1 {
|
|
return httperr.New(http.StatusForbidden, "CSRF value does not match")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FindUpstreamIDPByNameAndType finds the requested IDP by name and type, or returns an error.
|
|
// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values.
|
|
func FindUpstreamIDPByNameAndType(
|
|
idpLister UpstreamIdentityProvidersLister,
|
|
upstreamName string,
|
|
upstreamType string,
|
|
) (
|
|
provider.UpstreamOIDCIdentityProviderI,
|
|
provider.UpstreamLDAPIdentityProviderI,
|
|
psession.ProviderType,
|
|
error,
|
|
) {
|
|
switch upstreamType {
|
|
case string(v1alpha1.IDPTypeOIDC):
|
|
for _, p := range idpLister.GetOIDCIdentityProviders() {
|
|
if p.GetName() == upstreamName {
|
|
return p, nil, psession.ProviderTypeOIDC, nil
|
|
}
|
|
}
|
|
case string(v1alpha1.IDPTypeLDAP):
|
|
for _, p := range idpLister.GetLDAPIdentityProviders() {
|
|
if p.GetName() == upstreamName {
|
|
return nil, p, psession.ProviderTypeLDAP, nil
|
|
}
|
|
}
|
|
case string(v1alpha1.IDPTypeActiveDirectory):
|
|
for _, p := range idpLister.GetActiveDirectoryIdentityProviders() {
|
|
if p.GetName() == upstreamName {
|
|
return nil, p, psession.ProviderTypeActiveDirectory, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, nil, "", errors.New("provider not found")
|
|
}
|
|
|
|
// WriteAuthorizeError writes an authorization error as it should be returned by the authorization endpoint and other
|
|
// similar endpoints that are the end of the downstream authcode flow. Errors responses are written in the usual fosite style.
|
|
func WriteAuthorizeError(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) {
|
|
if plog.Enabled(plog.LevelTrace) {
|
|
// When trace level logging is enabled, include the stack trace in the log message.
|
|
keysAndValues := FositeErrorForLog(err)
|
|
errWithStack := errorsx.WithStack(err)
|
|
keysAndValues = append(keysAndValues, "errWithStack")
|
|
// klog always prints error values using %s, which does not include stack traces,
|
|
// so convert the error to a string which includes the stack trace here.
|
|
keysAndValues = append(keysAndValues, fmt.Sprintf("%+v", errWithStack))
|
|
plog.Trace("authorize response error", keysAndValues...)
|
|
} else {
|
|
plog.Info("authorize response error", FositeErrorForLog(err)...)
|
|
}
|
|
if isBrowserless {
|
|
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
|
|
}
|
|
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
|
|
oauthHelper.WriteAuthorizeError(r.Context(), w, authorizeRequester, err)
|
|
}
|
|
|
|
// PerformAuthcodeRedirect successfully completes a downstream login by creating a session and
|
|
// writing the authcode redirect response as it should be returned by the authorization endpoint and other
|
|
// similar endpoints that are the end of the downstream authcode flow.
|
|
func PerformAuthcodeRedirect(
|
|
r *http.Request,
|
|
w http.ResponseWriter,
|
|
oauthHelper fosite.OAuth2Provider,
|
|
authorizeRequester fosite.AuthorizeRequester,
|
|
openIDSession *psession.PinnipedSession,
|
|
isBrowserless bool,
|
|
) {
|
|
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
|
if err != nil {
|
|
plog.WarningErr("error while generating and saving authcode", err, "fositeErr", FositeErrorForLog(err))
|
|
WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, err, isBrowserless)
|
|
return
|
|
}
|
|
if isBrowserless {
|
|
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
|
|
}
|
|
oauthHelper.WriteAuthorizeResponse(r.Context(), w, authorizeRequester, authorizeResponder)
|
|
}
|
|
|
|
func rewriteStatusSeeOtherToStatusFoundForBrowserless(w http.ResponseWriter) http.ResponseWriter {
|
|
// rewrite http.StatusSeeOther to http.StatusFound for backwards compatibility with old pinniped CLIs.
|
|
// we can drop this in a few releases once we feel enough time has passed for users to update.
|
|
//
|
|
// WriteAuthorizeResponse/WriteAuthorizeError calls used to result in http.StatusFound until
|
|
// https://github.com/ory/fosite/pull/636 changed it to http.StatusSeeOther to address
|
|
// https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
|
|
// Safari has the bad behavior in the case of http.StatusFound and not just http.StatusTemporaryRedirect.
|
|
//
|
|
// in the browserless flows, the OAuth client is the pinniped CLI and it already has access to the user's
|
|
// password. Thus there is no security issue with using http.StatusFound vs. http.StatusSeeOther.
|
|
return httpsnoop.Wrap(w, httpsnoop.Hooks{
|
|
WriteHeader: func(delegate httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
|
|
return func(code int) {
|
|
if code == http.StatusSeeOther {
|
|
code = http.StatusFound
|
|
}
|
|
delegate(code)
|
|
}
|
|
},
|
|
})
|
|
}
|