Files
wonderwall/pkg/router/handler_callback.go
Trong Huu Nguyen 879319cd2a fix(router/login): alleviate SameSite issues for login cookie
A login cookie is set as part of the redirection flow between the RP
and OP, and thus inherently involves cross-site requests. Our client
uses the response_mode=query parameter for authorization requests, which
should work with the SameSite attribute set to Lax. However, there are
certain versions of user agents on certain operating systems (e.g.
Safari 12.2 on iOS<12.2, MacOS<10.14.4, Android WebView<72) that do not
properly handle cookies with the SameSite attribute set.

This commit attempts to alleviate this issue for legacy browsers by
introducing a fallback cookie without the SameSite attribute set.

Additionally, we also set the SameSite value for the original login
cookie to None to ensure that the cookie persists through the
cross-origin redirection requests.
2022-01-07 14:16:46 +01:00

110 lines
3.1 KiB
Go

package router
import (
"context"
"fmt"
"net/http"
"time"
"github.com/lestrrat-go/jwx/jwt"
"golang.org/x/oauth2"
"github.com/nais/wonderwall/pkg/openid"
)
func (h *Handler) Callback(w http.ResponseWriter, r *http.Request) {
loginCookie, err := h.getLoginCookie(r)
if err != nil {
h.Unauthorized(w, r, fmt.Errorf("callback: fetching login cookie: %w", err))
return
}
params := r.URL.Query()
if params.Get("error") != "" {
oauthError := params.Get("error")
oauthErrorDescription := params.Get("error_description")
h.InternalError(w, r, fmt.Errorf("callback: error from identity provider: %s: %s", oauthError, oauthErrorDescription))
return
}
if params.Get("state") != loginCookie.State {
h.Unauthorized(w, r, fmt.Errorf("callback: state parameter mismatch"))
return
}
tokens, err := h.codeExchangeForToken(r.Context(), loginCookie, params.Get("code"))
if err != nil {
h.InternalError(w, r, fmt.Errorf("callback: exchanging code: %w", err))
return
}
jwkSet := h.Provider.GetPublicJwkSet()
idToken, err := openid.ParseIDToken(*jwkSet, tokens)
if err != nil {
h.InternalError(w, r, fmt.Errorf("callback: parsing id_token: %w", err))
return
}
externalSessionID, err := h.validateIDToken(idToken, loginCookie)
if err != nil {
h.InternalError(w, r, fmt.Errorf("callback: validating id_token: %w", err))
return
}
err = h.createSession(w, r, externalSessionID, tokens, idToken)
if err != nil {
h.InternalError(w, r, fmt.Errorf("callback: creating session: %w", err))
return
}
h.clearLoginCookies(w)
http.Redirect(w, r, loginCookie.Referer, http.StatusTemporaryRedirect)
}
func (h *Handler) codeExchangeForToken(ctx context.Context, loginCookie *openid.LoginCookie, code string) (*oauth2.Token, error) {
clientAssertion, err := openid.ClientAssertion(h.Provider, time.Second*30)
if err != nil {
return nil, fmt.Errorf("creating client assertion: %w", err)
}
opts := []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("code_verifier", loginCookie.CodeVerifier),
oauth2.SetAuthURLParam("client_assertion", clientAssertion),
oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
}
tokens, err := h.OauthConfig.Exchange(ctx, code, opts...)
if err != nil {
return nil, fmt.Errorf("exchanging code for token: %w", err)
}
return tokens, nil
}
func (h *Handler) validateIDToken(idToken *openid.IDToken, loginCookie *openid.LoginCookie) (string, error) {
validateOpts := []jwt.ValidateOption{
jwt.WithAudience(h.Provider.GetClientConfiguration().GetClientID()),
jwt.WithClaimValue("nonce", loginCookie.Nonce),
jwt.WithIssuer(h.Provider.GetOpenIDConfiguration().Issuer),
jwt.WithAcceptableSkew(5 * time.Second),
jwt.WithRequiredClaim("sid"),
}
if len(h.Provider.GetClientConfiguration().GetACRValues()) > 0 {
validateOpts = append(validateOpts, jwt.WithRequiredClaim("acr"))
}
err := idToken.Validate(validateOpts...)
if err != nil {
return "", err
}
externalSessionID, err := idToken.GetSID()
if err != nil {
return "", fmt.Errorf("getting external session ID from id_token: %w", err)
}
return externalSessionID, nil
}