2020-11-04 00:17:38 +00:00
|
|
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
// Package auth provides a handler for the OIDC authorization endpoint.
|
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2020-11-06 22:44:58 +00:00
|
|
|
"time"
|
2020-11-04 20:19:07 +00:00
|
|
|
|
2020-11-12 23:36:59 +00:00
|
|
|
"github.com/gorilla/securecookie"
|
|
|
|
|
2020-11-04 23:04:50 +00:00
|
|
|
"github.com/ory/fosite"
|
2020-11-06 22:44:58 +00:00
|
|
|
"github.com/ory/fosite/handler/openid"
|
|
|
|
"github.com/ory/fosite/token/jwt"
|
2020-11-11 01:58:00 +00:00
|
|
|
"golang.org/x/oauth2"
|
|
|
|
|
2020-11-04 00:17:38 +00:00
|
|
|
"go.pinniped.dev/internal/httputil/httperr"
|
2020-11-11 01:58:00 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
2020-11-04 00:17:38 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/provider"
|
|
|
|
"go.pinniped.dev/internal/oidcclient/nonce"
|
|
|
|
"go.pinniped.dev/internal/oidcclient/pkce"
|
2020-11-11 01:58:00 +00:00
|
|
|
"go.pinniped.dev/internal/plog"
|
|
|
|
)
|
|
|
|
|
|
|
|
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.
|
2020-11-11 20:29:14 +00:00
|
|
|
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"
|
|
|
|
|
|
|
|
// The name of the browser cookie which shall hold our CSRF value.
|
|
|
|
// `__Host` prefix has a special meaning. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Cookie_prefixes
|
|
|
|
csrfCookieName = "__Host-pinniped-csrf"
|
2020-11-12 23:36:59 +00:00
|
|
|
|
|
|
|
// The `name` passed to the encoder for encoding and decoding the CSRF cookie contents.
|
|
|
|
csrfCookieEncodingName = "csrf"
|
2020-11-04 00:17:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type IDPListGetter interface {
|
|
|
|
GetIDPList() []provider.UpstreamOIDCIdentityProvider
|
|
|
|
}
|
|
|
|
|
2020-11-11 20:29:14 +00:00
|
|
|
// This is the encoding side of the securecookie.Codec interface.
|
2020-11-11 01:58:00 +00:00
|
|
|
type Encoder interface {
|
|
|
|
Encode(name string, value interface{}) (string, error)
|
|
|
|
}
|
|
|
|
|
2020-11-04 00:17:38 +00:00
|
|
|
func NewHandler(
|
|
|
|
issuer string,
|
|
|
|
idpListGetter IDPListGetter,
|
2020-11-04 23:04:50 +00:00
|
|
|
oauthHelper fosite.OAuth2Provider,
|
2020-11-11 01:58:00 +00:00
|
|
|
generateCSRF func() (csrftoken.CSRFToken, error),
|
2020-11-04 00:17:38 +00:00
|
|
|
generatePKCE func() (pkce.Code, error),
|
|
|
|
generateNonce func() (nonce.Nonce, error),
|
2020-11-12 23:36:59 +00:00
|
|
|
upstreamStateEncoder Encoder,
|
|
|
|
cookieCodec securecookie.Codec,
|
2020-11-04 00:17:38 +00:00
|
|
|
) http.Handler {
|
|
|
|
return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
if r.Method != http.MethodPost && r.Method != http.MethodGet {
|
|
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
|
|
// Authorization Servers MUST support the use of the HTTP GET and POST methods defined in
|
|
|
|
// RFC 2616 [RFC2616] at the Authorization Endpoint.
|
|
|
|
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
|
|
|
|
}
|
|
|
|
|
2020-11-12 23:36:59 +00:00
|
|
|
csrfFromCookie, err := readCSRFCookie(r, cookieCodec)
|
|
|
|
if err != nil {
|
|
|
|
plog.InfoErr("error reading CSRF cookie", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-11-04 23:04:50 +00:00
|
|
|
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r)
|
2020-11-04 15:15:19 +00:00
|
|
|
if err != nil {
|
2020-11-10 18:33:52 +00:00
|
|
|
plog.Info("authorize request error", fositeErrorForLog(err)...)
|
2020-11-04 15:15:19 +00:00
|
|
|
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-11-04 23:04:50 +00:00
|
|
|
upstreamIDP, err := chooseUpstreamIDP(idpListGetter)
|
2020-11-04 20:19:07 +00:00
|
|
|
if err != nil {
|
2020-11-11 20:29:14 +00:00
|
|
|
plog.WarningErr("authorize upstream config", err)
|
2020-11-04 23:04:50 +00:00
|
|
|
return err
|
2020-11-04 20:19:07 +00:00
|
|
|
}
|
|
|
|
|
2020-11-06 22:44:58 +00:00
|
|
|
// Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations.
|
2020-11-12 23:36:59 +00:00
|
|
|
grantOpenIDScopeIfRequested(authorizeRequester)
|
2020-11-06 22:44:58 +00:00
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
_, err = oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{
|
|
|
|
Claims: &jwt.IDTokenClaims{
|
|
|
|
// Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations.
|
|
|
|
Subject: "none",
|
|
|
|
AuthTime: now,
|
|
|
|
RequestedAt: now,
|
|
|
|
},
|
|
|
|
})
|
2020-11-04 00:17:38 +00:00
|
|
|
if err != nil {
|
2020-11-10 18:33:52 +00:00
|
|
|
plog.Info("authorize response error", fositeErrorForLog(err)...)
|
2020-11-04 23:04:50 +00:00
|
|
|
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
|
|
|
return nil
|
2020-11-04 00:17:38 +00:00
|
|
|
}
|
|
|
|
|
2020-11-11 01:58:00 +00:00
|
|
|
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
|
2020-11-04 00:17:38 +00:00
|
|
|
if err != nil {
|
2020-11-11 20:29:14 +00:00
|
|
|
plog.Error("authorize generate error", err)
|
2020-11-04 00:17:38 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-11-12 23:36:59 +00:00
|
|
|
if csrfFromCookie != "" {
|
|
|
|
csrfValue = csrfFromCookie
|
|
|
|
}
|
2020-11-04 00:17:38 +00:00
|
|
|
|
|
|
|
upstreamOAuthConfig := oauth2.Config{
|
|
|
|
ClientID: upstreamIDP.ClientID,
|
|
|
|
Endpoint: oauth2.Endpoint{
|
|
|
|
AuthURL: upstreamIDP.AuthorizationURL.String(),
|
|
|
|
},
|
|
|
|
RedirectURL: fmt.Sprintf("%s/callback/%s", issuer, upstreamIDP.Name),
|
|
|
|
Scopes: upstreamIDP.Scopes,
|
|
|
|
}
|
|
|
|
|
2020-11-12 23:36:59 +00:00
|
|
|
encodedStateParamValue, err := upstreamStateParam(authorizeRequester, nonceValue, csrfValue, pkceValue, upstreamStateEncoder)
|
2020-11-11 20:29:14 +00:00
|
|
|
if err != nil {
|
|
|
|
plog.Error("authorize upstream state param error", err)
|
|
|
|
return err
|
2020-11-11 01:58:00 +00:00
|
|
|
}
|
2020-11-11 20:29:14 +00:00
|
|
|
|
2020-11-12 23:36:59 +00:00
|
|
|
if csrfFromCookie == "" {
|
|
|
|
// We did not receive an incoming CSRF cookie, so write a new one.
|
|
|
|
err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
|
|
|
|
if err != nil {
|
|
|
|
plog.Error("error setting CSRF cookie", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2020-11-11 01:58:00 +00:00
|
|
|
|
2020-11-04 00:17:38 +00:00
|
|
|
http.Redirect(w, r,
|
|
|
|
upstreamOAuthConfig.AuthCodeURL(
|
2020-11-11 01:58:00 +00:00
|
|
|
encodedStateParamValue,
|
2020-11-04 00:17:38 +00:00
|
|
|
oauth2.AccessTypeOffline,
|
|
|
|
nonceValue.Param(),
|
|
|
|
pkceValue.Challenge(),
|
|
|
|
pkceValue.Method(),
|
|
|
|
),
|
|
|
|
302,
|
|
|
|
)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-11-12 23:36:59 +00:00
|
|
|
func readCSRFCookie(r *http.Request, codec securecookie.Codec) (csrftoken.CSRFToken, error) {
|
|
|
|
receivedCSRFCookie, err := r.Cookie(csrfCookieName)
|
|
|
|
if err != nil {
|
|
|
|
// Error means that the cookie was not found
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var csrfFromCookie csrftoken.CSRFToken
|
|
|
|
err = codec.Decode(csrfCookieEncodingName, receivedCSRFCookie.Value, &csrfFromCookie)
|
|
|
|
if err != nil {
|
|
|
|
return "", httperr.Wrap(http.StatusUnprocessableEntity, "error reading CSRF cookie", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return csrfFromCookie, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func grantOpenIDScopeIfRequested(authorizeRequester fosite.AuthorizeRequester) {
|
|
|
|
for _, scope := range authorizeRequester.GetRequestedScopes() {
|
|
|
|
if scope == "openid" {
|
|
|
|
authorizeRequester.GrantScope(scope)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-04 00:17:38 +00:00
|
|
|
func chooseUpstreamIDP(idpListGetter IDPListGetter) (*provider.UpstreamOIDCIdentityProvider, error) {
|
|
|
|
allUpstreamIDPs := idpListGetter.GetIDPList()
|
|
|
|
if len(allUpstreamIDPs) == 0 {
|
|
|
|
return nil, httperr.New(
|
|
|
|
http.StatusUnprocessableEntity,
|
|
|
|
"No upstream providers are configured",
|
|
|
|
)
|
|
|
|
} else if len(allUpstreamIDPs) > 1 {
|
|
|
|
return nil, httperr.New(
|
|
|
|
http.StatusUnprocessableEntity,
|
|
|
|
"Too many upstream providers are configured (support for multiple upstreams is not yet implemented)",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return &allUpstreamIDPs[0], nil
|
|
|
|
}
|
|
|
|
|
2020-11-11 01:58:00 +00:00
|
|
|
func generateValues(
|
|
|
|
generateCSRF func() (csrftoken.CSRFToken, error),
|
2020-11-04 00:17:38 +00:00
|
|
|
generateNonce func() (nonce.Nonce, error),
|
|
|
|
generatePKCE func() (pkce.Code, error),
|
2020-11-11 01:58:00 +00:00
|
|
|
) (csrftoken.CSRFToken, nonce.Nonce, pkce.Code, error) {
|
|
|
|
csrfValue, err := generateCSRF()
|
2020-11-04 00:17:38 +00:00
|
|
|
if err != nil {
|
2020-11-11 01:58:00 +00:00
|
|
|
return "", "", "", httperr.Wrap(http.StatusInternalServerError, "error generating CSRF token", err)
|
2020-11-04 00:17:38 +00:00
|
|
|
}
|
|
|
|
nonceValue, err := generateNonce()
|
|
|
|
if err != nil {
|
|
|
|
return "", "", "", httperr.Wrap(http.StatusInternalServerError, "error generating nonce param", err)
|
|
|
|
}
|
|
|
|
pkceValue, err := generatePKCE()
|
|
|
|
if err != nil {
|
|
|
|
return "", "", "", httperr.Wrap(http.StatusInternalServerError, "error generating PKCE param", err)
|
|
|
|
}
|
2020-11-11 01:58:00 +00:00
|
|
|
return csrfValue, nonceValue, pkceValue, nil
|
2020-11-04 00:17:38 +00:00
|
|
|
}
|
2020-11-10 18:33:52 +00:00
|
|
|
|
2020-11-11 20:29:14 +00:00
|
|
|
// 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"`
|
|
|
|
Nonce nonce.Nonce `json:"n"`
|
|
|
|
CSRFToken csrftoken.CSRFToken `json:"c"`
|
|
|
|
PKCECode pkce.Code `json:"k"`
|
|
|
|
StateParamFormatVersion string `json:"v"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func upstreamStateParam(
|
|
|
|
authorizeRequester fosite.AuthorizeRequester,
|
|
|
|
nonceValue nonce.Nonce,
|
|
|
|
csrfValue csrftoken.CSRFToken,
|
|
|
|
pkceValue pkce.Code,
|
|
|
|
encoder Encoder,
|
|
|
|
) (string, error) {
|
|
|
|
stateParamData := upstreamStateParamData{
|
|
|
|
AuthParams: authorizeRequester.GetRequestForm().Encode(),
|
|
|
|
Nonce: nonceValue,
|
|
|
|
CSRFToken: csrfValue,
|
|
|
|
PKCECode: pkceValue,
|
|
|
|
StateParamFormatVersion: upstreamStateParamFormatVersion,
|
|
|
|
}
|
|
|
|
encodedStateParamValue, err := encoder.Encode(upstreamStateParamEncodingName, stateParamData)
|
|
|
|
if err != nil {
|
|
|
|
return "", httperr.Wrap(http.StatusInternalServerError, "error encoding upstream state param", err)
|
|
|
|
}
|
|
|
|
return encodedStateParamValue, nil
|
|
|
|
}
|
|
|
|
|
2020-11-12 23:36:59 +00:00
|
|
|
func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken, codec securecookie.Codec) error {
|
|
|
|
encodedCSRFValue, err := codec.Encode(csrfCookieEncodingName, csrfValue)
|
|
|
|
if err != nil {
|
|
|
|
return httperr.Wrap(http.StatusInternalServerError, "error encoding CSRF cookie", err)
|
|
|
|
}
|
|
|
|
|
2020-11-11 20:29:14 +00:00
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
Name: csrfCookieName,
|
2020-11-12 23:36:59 +00:00
|
|
|
Value: encodedCSRFValue,
|
2020-11-11 20:29:14 +00:00
|
|
|
HttpOnly: true,
|
|
|
|
SameSite: http.SameSiteStrictMode,
|
|
|
|
Secure: true,
|
|
|
|
})
|
2020-11-12 23:36:59 +00:00
|
|
|
|
|
|
|
return nil
|
2020-11-11 20:29:14 +00:00
|
|
|
}
|
|
|
|
|
2020-11-10 18:33:52 +00:00
|
|
|
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
|
|
|
|
}
|