Files
wonderwall/pkg/handler/handler_callback.go
2022-08-26 14:32:39 +02:00

130 lines
3.6 KiB
Go

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()
}