51c828382f
- This commit does not include the sad path tests for the refresh grant type, which will come in a future commit.
169 lines
6.3 KiB
Go
169 lines
6.3 KiB
Go
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package oidc contains common OIDC functionality needed by Pinniped.
|
|
package oidc
|
|
|
|
import (
|
|
"time"
|
|
|
|
coreosoidc "github.com/coreos/go-oidc"
|
|
"github.com/ory/fosite"
|
|
"github.com/ory/fosite/compose"
|
|
|
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
|
"go.pinniped.dev/internal/oidc/jwks"
|
|
"go.pinniped.dev/internal/oidc/provider"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
// 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.
|
|
UpstreamStateParamFormatVersion = "1"
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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"`
|
|
Nonce nonce.Nonce `json:"n"`
|
|
CSRFToken csrftoken.CSRFToken `json:"c"`
|
|
PKCECode pkce.Code `json:"k"`
|
|
FormatVersion string `json:"v"`
|
|
}
|
|
|
|
func PinnipedCLIOIDCClient() *fosite.DefaultOpenIDConnectClient {
|
|
return &fosite.DefaultOpenIDConnectClient{
|
|
DefaultClient: &fosite.DefaultClient{
|
|
ID: "pinniped-cli",
|
|
Public: true,
|
|
RedirectURIs: []string{"http://127.0.0.1/callback"},
|
|
ResponseTypes: []string{"code"},
|
|
GrantTypes: []string{"authorization_code", "refresh_token"},
|
|
Scopes: []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, "profile", "email"},
|
|
},
|
|
TokenEndpointAuthMethod: "none",
|
|
}
|
|
}
|
|
|
|
func FositeOauth2Helper(
|
|
oauthStore interface{},
|
|
issuer string,
|
|
hmacSecretOfLengthAtLeast32 []byte,
|
|
jwksProvider jwks.DynamicJWKSProvider,
|
|
) fosite.OAuth2Provider {
|
|
oauthConfig := &compose.Config{
|
|
AuthorizeCodeLifespan: 3 * time.Minute, // seems more than long enough to exchange a code
|
|
|
|
IDTokenLifespan: 5 * time.Minute, // match clientCertificateTTL since it has similar properties to this token
|
|
AccessTokenLifespan: 5 * time.Minute, // match clientCertificateTTL since it has similar properties to this token
|
|
|
|
RefreshTokenLifespan: 16 * time.Hour, // long enough for a single workday
|
|
|
|
IDTokenIssuer: issuer,
|
|
|
|
ScopeStrategy: fosite.ExactScopeStrategy, // be careful and only support exact string matching for scopes
|
|
AudienceMatchingStrategy: nil, // I believe the default is fine
|
|
EnforcePKCE: true, // follow current set of best practices and always require PKCE
|
|
AllowedPromptValues: []string{"none"}, // TODO unclear what we should set here
|
|
|
|
RefreshTokenScopes: []string{coreosoidc.ScopeOfflineAccess}, // as per https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
|
|
|
MinParameterEntropy: 32, // 256 bits seems about right
|
|
}
|
|
|
|
return compose.Compose(
|
|
oauthConfig,
|
|
oauthStore,
|
|
&compose.CommonStrategy{
|
|
// Note that Fosite requires the HMAC secret to be at least 32 bytes.
|
|
CoreStrategy: compose.NewOAuth2HMACStrategy(oauthConfig, hmacSecretOfLengthAtLeast32, nil),
|
|
OpenIDConnectTokenStrategy: newDynamicOpenIDConnectECDSAStrategy(oauthConfig, jwksProvider),
|
|
},
|
|
nil, // hasher, defaults to using BCrypt when nil. Used for hashing client secrets.
|
|
compose.OAuth2AuthorizeExplicitFactory,
|
|
compose.OAuth2RefreshTokenGrantFactory,
|
|
compose.OpenIDConnectExplicitFactory,
|
|
compose.OpenIDConnectRefreshFactory,
|
|
compose.OAuth2PKCEFactory,
|
|
)
|
|
}
|
|
|
|
// 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.Name)
|
|
keysAndValues = append(keysAndValues, "status")
|
|
keysAndValues = append(keysAndValues, rfc6749Error.Status())
|
|
keysAndValues = append(keysAndValues, "description")
|
|
keysAndValues = append(keysAndValues, rfc6749Error.Description)
|
|
return keysAndValues
|
|
}
|
|
|
|
type IDPListGetter interface {
|
|
GetIDPList() []provider.UpstreamOIDCIdentityProviderI
|
|
}
|
|
|
|
func GrantScopeIfRequested(authorizeRequester fosite.AuthorizeRequester, scopeName string) {
|
|
for _, scope := range authorizeRequester.GetRequestedScopes() {
|
|
if scope == scopeName {
|
|
authorizeRequester.GrantScope(scope)
|
|
}
|
|
}
|
|
}
|