mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-29 18:36:30 +02:00
Update the initialization logic for the authenticate, authorize, and proxy services to automatically select between the stateful authentication flow and the stateless authentication flow, depending on whether Pomerium is configured to use the hosted authenticate service. Add a unit test case to verify that the sign_out handler does not trigger a sign in redirect.
536 lines
18 KiB
Go
536 lines
18 KiB
Go
package authenticate
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/mux"
|
|
"github.com/rs/cors"
|
|
|
|
"github.com/pomerium/csrf"
|
|
"github.com/pomerium/pomerium/internal/authenticateflow"
|
|
"github.com/pomerium/pomerium/internal/handlers"
|
|
"github.com/pomerium/pomerium/internal/httputil"
|
|
"github.com/pomerium/pomerium/internal/identity"
|
|
"github.com/pomerium/pomerium/internal/identity/oidc"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/middleware"
|
|
"github.com/pomerium/pomerium/internal/sessions"
|
|
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
|
"github.com/pomerium/pomerium/internal/urlutil"
|
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
)
|
|
|
|
// Handler returns the authenticate service's handler chain.
|
|
func (a *Authenticate) Handler() http.Handler {
|
|
r := httputil.NewRouter()
|
|
a.Mount(r)
|
|
return r
|
|
}
|
|
|
|
// Mount mounts the authenticate routes to the given router.
|
|
func (a *Authenticate) Mount(r *mux.Router) {
|
|
r.StrictSlash(true)
|
|
r.Use(middleware.SetHeaders(httputil.HeadersContentSecurityPolicy))
|
|
r.Use(func(h http.Handler) http.Handler {
|
|
options := a.options.Load()
|
|
state := a.state.Load()
|
|
csrfKey := fmt.Sprintf("%s_csrf", options.CookieName)
|
|
csrfOptions := []csrf.Option{
|
|
csrf.Secure(options.CookieSecure),
|
|
csrf.Path("/"),
|
|
csrf.UnsafePaths(
|
|
[]string{
|
|
"/oauth2/callback", // rfc6749#section-10.12 accepts GET
|
|
}),
|
|
csrf.FormValueName("state"), // rfc6749#section-10.12
|
|
csrf.CookieName(csrfKey),
|
|
csrf.FieldName(csrfKey),
|
|
csrf.ErrorHandler(httputil.HandlerFunc(httputil.CSRFFailureHandler)),
|
|
csrf.SameSite(options.GetCSRFSameSite()),
|
|
}
|
|
return csrf.Protect(state.cookieSecret, csrfOptions...)(h)
|
|
})
|
|
|
|
// redirect / to /.pomerium/
|
|
r.Path("/").Handler(http.RedirectHandler("/.pomerium/", http.StatusFound))
|
|
|
|
r.Path("/robots.txt").HandlerFunc(a.RobotsTxt).Methods(http.MethodGet)
|
|
// Identity Provider (IdP) endpoints
|
|
r.Path("/oauth2/callback").Handler(httputil.HandlerFunc(a.OAuthCallback)).Methods(http.MethodGet, http.MethodPost)
|
|
|
|
a.mountDashboard(r)
|
|
}
|
|
|
|
func (a *Authenticate) mountDashboard(r *mux.Router) {
|
|
sr := httputil.DashboardSubrouter(r)
|
|
c := cors.New(cors.Options{
|
|
AllowOriginRequestFunc: func(r *http.Request, _ string) bool {
|
|
state := a.state.Load()
|
|
err := state.flow.VerifyAuthenticateSignature(r)
|
|
if err != nil {
|
|
log.FromRequest(r).Info().Err(err).Msg("authenticate: origin blocked")
|
|
}
|
|
return err == nil
|
|
},
|
|
AllowCredentials: true,
|
|
AllowedHeaders: []string{"*"},
|
|
})
|
|
sr.Use(c.Handler)
|
|
sr.Use(a.RetrieveSession)
|
|
|
|
// routes that don't need a session:
|
|
sr.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut))
|
|
sr.Path("/signed_out").Handler(handlers.SignedOut(handlers.SignedOutData{})).Methods(http.MethodGet)
|
|
|
|
// routes that need a session:
|
|
sr = sr.NewRoute().Subrouter()
|
|
sr.Use(a.VerifySession)
|
|
sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo))
|
|
sr.Path("/sign_in").Handler(httputil.HandlerFunc(a.SignIn))
|
|
sr.Path("/device-enrolled").Handler(httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
handlers.DeviceEnrolled(a.getUserInfoData(r)).ServeHTTP(w, r)
|
|
return nil
|
|
}))
|
|
|
|
cr := sr.PathPrefix("/callback").Subrouter()
|
|
cr.Path("/").Handler(a.requireValidSignature(a.Callback)).Methods(http.MethodGet)
|
|
}
|
|
|
|
// RetrieveSession is the middleware used retrieve session by the sessionLoader
|
|
func (a *Authenticate) RetrieveSession(next http.Handler) http.Handler {
|
|
return sessions.RetrieveSession(a.state.Load().sessionLoader)(next)
|
|
}
|
|
|
|
// VerifySession is the middleware used to enforce a valid authentication
|
|
// session state is attached to the users's request context.
|
|
func (a *Authenticate) VerifySession(next http.Handler) http.Handler {
|
|
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
ctx, span := trace.StartSpan(r.Context(), "authenticate.VerifySession")
|
|
defer span.End()
|
|
|
|
state := a.state.Load()
|
|
idpID := a.getIdentityProviderIDForRequest(r)
|
|
|
|
sessionState, err := a.getSessionFromCtx(ctx)
|
|
if err != nil {
|
|
log.FromRequest(r).Info().
|
|
Err(err).
|
|
Str("idp_id", idpID).
|
|
Msg("authenticate: session load error")
|
|
return a.reauthenticateOrFail(w, r, err)
|
|
}
|
|
|
|
if sessionState.IdentityProviderID != idpID {
|
|
log.FromRequest(r).Info().
|
|
Str("idp_id", idpID).
|
|
Str("session_idp_id", sessionState.IdentityProviderID).
|
|
Str("id", sessionState.ID).
|
|
Msg("authenticate: session not associated with identity provider")
|
|
return a.reauthenticateOrFail(w, r, err)
|
|
}
|
|
|
|
if err := state.flow.VerifySession(ctx, r, sessionState); err != nil {
|
|
log.FromRequest(r).Info().
|
|
Err(err).
|
|
Str("idp_id", idpID).
|
|
Msg("authenticate: couldn't verify session")
|
|
return a.reauthenticateOrFail(w, r, err)
|
|
}
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// RobotsTxt handles the /robots.txt route.
|
|
func (a *Authenticate) RobotsTxt(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
|
|
}
|
|
|
|
// SignIn handles authenticating a user.
|
|
func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error {
|
|
ctx, span := trace.StartSpan(r.Context(), "authenticate.SignIn")
|
|
defer span.End()
|
|
|
|
state := a.state.Load()
|
|
|
|
s, err := a.getSessionFromCtx(ctx)
|
|
if err != nil {
|
|
state.sessionStore.ClearSession(w, r)
|
|
return err
|
|
}
|
|
|
|
return state.flow.SignIn(w, r, s)
|
|
}
|
|
|
|
// SignOut signs the user out and attempts to revoke the user's identity session
|
|
// Handles both GET and POST.
|
|
func (a *Authenticate) SignOut(w http.ResponseWriter, r *http.Request) error {
|
|
// check for an HMAC'd URL. If none is found, show a confirmation page.
|
|
err := a.state.Load().flow.VerifyAuthenticateSignature(r)
|
|
if err != nil {
|
|
authenticateURL, err := a.options.Load().GetAuthenticateURL()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
handlers.SignOutConfirm(handlers.SignOutConfirmData{
|
|
URL: urlutil.SignOutURL(r, authenticateURL, a.state.Load().sharedKey),
|
|
}).ServeHTTP(w, r)
|
|
return nil
|
|
}
|
|
|
|
// otherwise actually do the sign out
|
|
return a.signOutRedirect(w, r)
|
|
}
|
|
|
|
func (a *Authenticate) signOutRedirect(w http.ResponseWriter, r *http.Request) error {
|
|
ctx, span := trace.StartSpan(r.Context(), "authenticate.SignOut")
|
|
defer span.End()
|
|
|
|
options := a.options.Load()
|
|
idpID := a.getIdentityProviderIDForRequest(r)
|
|
|
|
authenticator, err := a.cfg.getIdentityProvider(options, idpID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rawIDToken := a.revokeSession(ctx, w, r)
|
|
|
|
authenticateURL, err := options.GetAuthenticateURL()
|
|
if err != nil {
|
|
return fmt.Errorf("error getting authenticate url: %w", err)
|
|
}
|
|
|
|
signOutRedirectURL, err := options.GetSignOutRedirectURL()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var signOutURL string
|
|
if uri := r.FormValue(urlutil.QueryRedirectURI); uri != "" {
|
|
signOutURL = uri
|
|
} else if signOutRedirectURL != nil {
|
|
signOutURL = signOutRedirectURL.String()
|
|
} else {
|
|
signOutURL = authenticateURL.ResolveReference(&url.URL{
|
|
Path: "/.pomerium/signed_out",
|
|
}).String()
|
|
}
|
|
|
|
if idpSignOutURL, err := authenticator.GetSignOutURL(rawIDToken, signOutURL); err == nil {
|
|
signOutURL = idpSignOutURL
|
|
} else if !errors.Is(err, oidc.ErrSignoutNotImplemented) {
|
|
log.Warn(r.Context()).Err(err).Msg("authenticate: failed to get sign out url for authenticator")
|
|
}
|
|
|
|
httputil.Redirect(w, r, signOutURL, http.StatusFound)
|
|
return nil
|
|
}
|
|
|
|
// reauthenticateOrFail starts the authenticate process by redirecting the
|
|
// user to their respective identity provider. This function also builds the
|
|
// 'state' parameter which is encrypted and includes authenticating data
|
|
// for validation.
|
|
// If the request is a `xhr/ajax` request (e.g the `X-Requested-With` header)
|
|
// is set do not redirect but instead return 401 unauthorized.
|
|
//
|
|
// https://openid.net/specs/openid-connect-core-1_0-final.html#AuthRequest
|
|
// https://tools.ietf.org/html/rfc6749#section-4.2.1
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
|
|
func (a *Authenticate) reauthenticateOrFail(w http.ResponseWriter, r *http.Request, err error) error {
|
|
// If request AJAX/XHR request, return a 401 instead because the redirect
|
|
// will almost certainly violate their CORs policy
|
|
if reqType := r.Header.Get("X-Requested-With"); strings.EqualFold(reqType, "XmlHttpRequest") {
|
|
return httputil.NewError(http.StatusUnauthorized, err)
|
|
}
|
|
|
|
state := a.state.Load()
|
|
options := a.options.Load()
|
|
idpID := a.getIdentityProviderIDForRequest(r)
|
|
|
|
authenticator, err := a.cfg.getIdentityProvider(options, idpID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
state.flow.LogAuthenticateEvent(r)
|
|
|
|
state.sessionStore.ClearSession(w, r)
|
|
redirectURL := state.redirectURL.ResolveReference(r.URL)
|
|
nonce := csrf.Token(r)
|
|
now := time.Now().Unix()
|
|
b := []byte(fmt.Sprintf("%s|%d|", nonce, now))
|
|
enc := cryptutil.Encrypt(state.cookieCipher, []byte(redirectURL.String()), b)
|
|
b = append(b, enc...)
|
|
encodedState := base64.URLEncoding.EncodeToString(b)
|
|
signinURL, err := authenticator.GetSignInURL(encodedState)
|
|
if err != nil {
|
|
return httputil.NewError(http.StatusInternalServerError,
|
|
fmt.Errorf("failed to get sign in url: %w", err))
|
|
}
|
|
httputil.Redirect(w, r, signinURL, http.StatusFound)
|
|
return nil
|
|
}
|
|
|
|
// OAuthCallback handles the callback from the identity provider.
|
|
//
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
|
|
func (a *Authenticate) OAuthCallback(w http.ResponseWriter, r *http.Request) error {
|
|
redirect, err := a.getOAuthCallback(w, r)
|
|
if err != nil {
|
|
return fmt.Errorf("authenticate.OAuthCallback: %w", err)
|
|
}
|
|
httputil.Redirect(w, r, redirect.String(), http.StatusFound)
|
|
return nil
|
|
}
|
|
|
|
func (a *Authenticate) statusForErrorCode(errorCode string) int {
|
|
switch errorCode {
|
|
case "access_denied", "unauthorized_client":
|
|
return http.StatusUnauthorized
|
|
default:
|
|
return http.StatusBadRequest
|
|
}
|
|
}
|
|
|
|
func (a *Authenticate) getOAuthCallback(w http.ResponseWriter, r *http.Request) (*url.URL, error) {
|
|
ctx, span := trace.StartSpan(r.Context(), "authenticate.getOAuthCallback")
|
|
defer span.End()
|
|
|
|
state := a.state.Load()
|
|
options := a.options.Load()
|
|
|
|
// Error Authentication Response: rfc6749#section-4.1.2.1 & OIDC#3.1.2.6
|
|
//
|
|
// first, check if the identity provider returned an error
|
|
if idpError := r.FormValue("error"); idpError != "" {
|
|
return nil, httputil.NewError(a.statusForErrorCode(idpError), fmt.Errorf("identity provider: %v", idpError))
|
|
}
|
|
|
|
// fail if no session redemption code is returned
|
|
code := r.FormValue("code")
|
|
if code == "" {
|
|
return nil, httputil.NewError(http.StatusBadRequest, fmt.Errorf("identity provider returned empty code"))
|
|
}
|
|
|
|
// state includes a csrf nonce (validated by middleware) and redirect uri
|
|
bytes, err := base64.URLEncoding.DecodeString(r.FormValue("state"))
|
|
if err != nil {
|
|
return nil, httputil.NewError(http.StatusBadRequest, fmt.Errorf("bad bytes: %w", err))
|
|
}
|
|
|
|
// split state into concat'd components
|
|
// (nonce|timestamp|redirect_url|encrypted_data(redirect_url)+mac(nonce,ts))
|
|
statePayload := strings.SplitN(string(bytes), "|", 3)
|
|
if len(statePayload) != 3 {
|
|
return nil, httputil.NewError(http.StatusBadRequest, fmt.Errorf("state malformed, size: %d", len(statePayload)))
|
|
}
|
|
|
|
// Use our AEAD construct to enforce secrecy and authenticity:
|
|
// mac: to validate the nonce again, and above timestamp
|
|
// decrypt: to prevent leaking 'redirect_uri' to IdP or logs
|
|
b := []byte(fmt.Sprint(statePayload[0], "|", statePayload[1], "|"))
|
|
redirectString, err := cryptutil.Decrypt(state.cookieCipher, []byte(statePayload[2]), b)
|
|
if err != nil {
|
|
return nil, httputil.NewError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
redirectURL, err := urlutil.ParseAndValidateURL(string(redirectString))
|
|
if err != nil {
|
|
return nil, httputil.NewError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
// verify that the returned timestamp is valid
|
|
if err := cryptutil.ValidTimestamp(statePayload[1]); err != nil {
|
|
return nil, httputil.NewError(http.StatusBadRequest, err).WithDescription(fmt.Sprintf(`
|
|
The request expired. This may be because a login attempt took too long, or because the server's clock is out of sync.
|
|
|
|
Try again by following this link: [%s](%s).
|
|
|
|
Or contact your administrator.
|
|
`, redirectURL.String(), redirectURL.String()))
|
|
}
|
|
|
|
idpID := state.flow.GetIdentityProviderIDForURLValues(redirectURL.Query())
|
|
|
|
authenticator, err := a.cfg.getIdentityProvider(options, idpID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Successful Authentication Response: rfc6749#section-4.1.2 & OIDC#3.1.2.5
|
|
//
|
|
// Exchange the supplied Authorization Code for a valid user session.
|
|
var claims identity.SessionClaims
|
|
accessToken, err := authenticator.Authenticate(ctx, code, &claims)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error redeeming authenticate code: %w", err)
|
|
}
|
|
|
|
s := sessions.NewState(idpID)
|
|
err = claims.Claims.Claims(&s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error unmarshaling session state: %w", err)
|
|
}
|
|
|
|
newState := s.WithNewIssuer(state.redirectURL.Hostname(), []string{state.redirectURL.Hostname()})
|
|
if nextRedirectURL, err := urlutil.ParseAndValidateURL(redirectURL.Query().Get(urlutil.QueryRedirectURI)); err == nil {
|
|
newState.Audience = append(newState.Audience, nextRedirectURL.Hostname())
|
|
}
|
|
|
|
// save the session and access token to the databroker/cookie store
|
|
if err := state.flow.PersistSession(ctx, w, &newState, claims, accessToken); err != nil {
|
|
return nil, fmt.Errorf("failed saving new session: %w", err)
|
|
}
|
|
|
|
// ... and the user state to local storage.
|
|
if err := state.sessionStore.SaveSession(w, r, &newState); err != nil {
|
|
return nil, fmt.Errorf("failed saving new session: %w", err)
|
|
}
|
|
return redirectURL, nil
|
|
}
|
|
|
|
func (a *Authenticate) getSessionFromCtx(ctx context.Context) (*sessions.State, error) {
|
|
state := a.state.Load()
|
|
|
|
jwt, err := sessions.FromContext(ctx)
|
|
if err != nil {
|
|
return nil, httputil.NewError(http.StatusBadRequest, err)
|
|
}
|
|
var s sessions.State
|
|
if err := state.sharedEncoder.Unmarshal([]byte(jwt), &s); err != nil {
|
|
return nil, httputil.NewError(http.StatusBadRequest, err)
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
|
ctx, span := trace.StartSpan(r.Context(), "authenticate.userInfo")
|
|
defer span.End()
|
|
|
|
options := a.options.Load()
|
|
|
|
r = r.WithContext(ctx)
|
|
r = authenticateflow.GetExternalAuthenticateRequest(r, options)
|
|
|
|
// if we came in with a redirect URI, save it to a cookie so it doesn't expire with the HMAC
|
|
if redirectURI := r.FormValue(urlutil.QueryRedirectURI); redirectURI != "" {
|
|
u := urlutil.GetAbsoluteURL(r)
|
|
u.RawQuery = ""
|
|
|
|
cookie := options.NewCookie()
|
|
cookie.Name = urlutil.QueryRedirectURI
|
|
cookie.Value = redirectURI
|
|
|
|
http.SetCookie(w, cookie)
|
|
http.Redirect(w, r, u.String(), http.StatusFound)
|
|
return nil
|
|
}
|
|
|
|
handlers.UserInfo(a.getUserInfoData(r)).ServeHTTP(w, r)
|
|
return nil
|
|
}
|
|
|
|
func (a *Authenticate) getUserInfoData(r *http.Request) handlers.UserInfoData {
|
|
state := a.state.Load()
|
|
|
|
s, err := a.getSessionFromCtx(r.Context())
|
|
if err != nil {
|
|
s.ID = uuid.New().String()
|
|
}
|
|
|
|
data := state.flow.GetUserInfoData(r, s)
|
|
data.CSRFToken = csrf.Token(r)
|
|
data.BrandingOptions = a.options.Load().BrandingOptions
|
|
return data
|
|
}
|
|
|
|
// revokeSession always clears the local session and tries to revoke the associated session stored in the
|
|
// databroker. If successful, it returns the original `id_token` of the session, if failed, returns
|
|
// and empty string.
|
|
func (a *Authenticate) revokeSession(ctx context.Context, w http.ResponseWriter, r *http.Request) string {
|
|
state := a.state.Load()
|
|
options := a.options.Load()
|
|
|
|
// clear the user's local session no matter what
|
|
defer state.sessionStore.ClearSession(w, r)
|
|
|
|
idpID := r.FormValue(urlutil.QueryIdentityProviderID)
|
|
|
|
authenticator, err := a.cfg.getIdentityProvider(options, idpID)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
sessionState, _ := a.getSessionFromCtx(ctx)
|
|
|
|
return state.flow.RevokeSession(ctx, r, authenticator, sessionState)
|
|
}
|
|
|
|
// Callback handles the result of a successful call to the authenticate service
|
|
// and is responsible setting per-route sessions.
|
|
func (a *Authenticate) Callback(w http.ResponseWriter, r *http.Request) error {
|
|
redirectURLString := r.FormValue(urlutil.QueryRedirectURI)
|
|
encryptedSession := r.FormValue(urlutil.QuerySessionEncrypted)
|
|
|
|
redirectURL, err := urlutil.ParseAndValidateURL(redirectURLString)
|
|
if err != nil {
|
|
return httputil.NewError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
rawJWT, err := a.saveCallbackSession(w, r, encryptedSession)
|
|
if err != nil {
|
|
return httputil.NewError(http.StatusBadRequest, err)
|
|
}
|
|
|
|
// if programmatic, encode the session jwt as a query param
|
|
if isProgrammatic := r.FormValue(urlutil.QueryIsProgrammatic); isProgrammatic == "true" {
|
|
q := redirectURL.Query()
|
|
q.Set(urlutil.QueryPomeriumJWT, string(rawJWT))
|
|
redirectURL.RawQuery = q.Encode()
|
|
}
|
|
httputil.Redirect(w, r, redirectURL.String(), http.StatusFound)
|
|
return nil
|
|
}
|
|
|
|
// saveCallbackSession takes an encrypted per-route session token, decrypts
|
|
// it using the shared service key, then stores it the local session store.
|
|
func (a *Authenticate) saveCallbackSession(w http.ResponseWriter, r *http.Request, enctoken string) ([]byte, error) {
|
|
state := a.state.Load()
|
|
|
|
// 1. extract the base64 encoded and encrypted JWT from query params
|
|
encryptedJWT, err := base64.URLEncoding.DecodeString(enctoken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("proxy: malfromed callback token: %w", err)
|
|
}
|
|
// 2. decrypt the JWT using the cipher using the _shared_ secret key
|
|
rawJWT, err := cryptutil.Decrypt(state.sharedCipher, encryptedJWT, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("proxy: callback token decrypt error: %w", err)
|
|
}
|
|
// 3. Save the decrypted JWT to the session store directly as a string, without resigning
|
|
if err = state.sessionStore.SaveSession(w, r, rawJWT); err != nil {
|
|
return nil, fmt.Errorf("proxy: callback session save failure: %w", err)
|
|
}
|
|
return rawJWT, nil
|
|
}
|
|
|
|
func (a *Authenticate) getIdentityProviderIDForRequest(r *http.Request) string {
|
|
if err := r.ParseForm(); err != nil {
|
|
return ""
|
|
}
|
|
return a.state.Load().flow.GetIdentityProviderIDForURLValues(r.Form)
|
|
}
|