package handler import ( "context" "errors" "fmt" "net/http" "github.com/sethvargo/go-retry" log "github.com/sirupsen/logrus" "github.com/nais/wonderwall/pkg/cookie" "github.com/nais/wonderwall/pkg/loginstatus" "github.com/nais/wonderwall/pkg/metrics" logentry "github.com/nais/wonderwall/pkg/middleware" "github.com/nais/wonderwall/pkg/openid" "github.com/nais/wonderwall/pkg/openid/client" retrypkg "github.com/nais/wonderwall/pkg/retry" ) // Callback handles the authentication response from the identity provider. func (h *Handler) Callback(w http.ResponseWriter, r *http.Request) { // unconditionally clear login cookie h.clearLoginCookies(w, r) loginCookie, err := h.getLoginCookie(r) if err != nil { msg := "callback: fetching login cookie" if errors.Is(err, http.ErrNoCookie) { msg += ": fallback cookie not found (user might have blocked all cookies, or the callback route was accessed before the login route)" } h.Unauthorized(w, r, fmt.Errorf("%s: %w", msg, err)) return } loginCallback, err := h.Client.LoginCallback(r, h.Provider, loginCookie) if err != nil { h.InternalError(w, r, err) return } if err := loginCallback.IdentityProviderError(); err != nil { h.InternalError(w, r, fmt.Errorf("callback: %w", err)) return } if err := loginCallback.StateMismatchError(); err != nil { h.Unauthorized(w, r, fmt.Errorf("callback: %w", err)) return } tokens, err := h.redeemValidTokens(r, loginCallback) if err != nil { h.InternalError(w, r, fmt.Errorf("callback: redeeming tokens: %w", err)) return } sessionLifetime := h.Config.Session.MaxLifetime key, err := h.Sessions.Create(r, tokens, sessionLifetime) if err != nil { h.InternalError(w, r, fmt.Errorf("callback: creating session: %w", err)) return } opts := h.CookieOptsPathAware(r). WithExpiresIn(sessionLifetime) err = cookie.EncryptAndSet(w, cookie.Session, key, opts, h.Crypter) if err != nil { h.InternalError(w, r, fmt.Errorf("callback: setting session cookie: %w", err)) return } if h.Loginstatus.Enabled() { tokenResponse, err := h.getLoginstatusToken(r, tokens) if err != nil { h.InternalError(w, r, fmt.Errorf("callback: exchanging loginstatus token: %w", err)) return } h.Loginstatus.SetCookie(w, tokenResponse, h.CookieOptions) logentry.LogEntryFrom(r).Debug("callback: successfully fetched loginstatus token") } logSuccessfulLogin(r, tokens, loginCookie.Referer) http.Redirect(w, r, loginCookie.Referer, http.StatusTemporaryRedirect) } func (h *Handler) redeemValidTokens(r *http.Request, loginCallback client.LoginCallback) (*openid.Tokens, error) { var tokens *openid.Tokens var err error retryable := func(ctx context.Context) error { tokens, err = loginCallback.RedeemTokens(ctx) return retry.RetryableError(err) } if err := retry.Do(r.Context(), retrypkg.DefaultBackoff, retryable); err != nil { return nil, err } return tokens, nil } func (h *Handler) getLoginstatusToken(r *http.Request, tokens *openid.Tokens) (*loginstatus.TokenResponse, error) { var tokenResponse *loginstatus.TokenResponse retryable := func(ctx context.Context) error { var err error tokenResponse, err = h.Loginstatus.ExchangeToken(ctx, tokens.AccessToken) return retry.RetryableError(err) } if err := retry.Do(r.Context(), retrypkg.DefaultBackoff, retryable); err != nil { return nil, err } return tokenResponse, nil } func logSuccessfulLogin(r *http.Request, tokens *openid.Tokens, referer string) { fields := log.Fields{ "redirect_to": referer, "jti": tokens.IDToken.GetJwtID(), } logentry.LogEntryFrom(r).WithFields(fields).Info("callback: successful login") metrics.ObserveLogin() }