mirror of
https://github.com/nais/wonderwall.git
synced 2026-08-23 21:16:14 +00:00
feat(handler/login): add support for prompt param in login
This commit is contained in:
+27
-1
@@ -139,10 +139,36 @@ func (s *Standalone) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
logger := mw.LogEntryFrom(r)
|
||||
fields := log.Fields{
|
||||
"redirect_after_login": canonicalRedirect,
|
||||
}
|
||||
mw.LogEntryFrom(r).WithFields(fields).Info("login: redirecting to identity provider")
|
||||
|
||||
if acr := login.Acr; acr != "" {
|
||||
fields["acr"] = acr
|
||||
}
|
||||
|
||||
if locale := login.Locale; locale != "" {
|
||||
fields["locale"] = locale
|
||||
}
|
||||
|
||||
if prompt := login.Prompt; prompt != "" {
|
||||
fields["prompt"] = prompt
|
||||
logger.Infof("login: prompt='%s'; clearing local session...", prompt)
|
||||
|
||||
sess, _ := s.SessionManager.Get(r)
|
||||
if sess != nil {
|
||||
err := s.SessionManager.Delete(r.Context(), sess)
|
||||
if err != nil && !errors.Is(err, session.ErrNotFound) {
|
||||
s.InternalError(w, r, fmt.Errorf("login: destroying session: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cookie.Clear(w, cookie.Session, s.GetCookieOptions(r))
|
||||
}
|
||||
|
||||
logger.WithFields(fields).Info("login: redirecting to identity provider")
|
||||
http.Redirect(w, r, login.AuthCodeURL, http.StatusFound)
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,9 @@ func (s *SSOProxy) Login(w http.ResponseWriter, r *http.Request) {
|
||||
if reqQuery.Has(openidclient.LocaleURLParameter) {
|
||||
targetQuery.Set(openidclient.LocaleURLParameter, reqQuery.Get(openidclient.LocaleURLParameter))
|
||||
}
|
||||
if reqQuery.Has(openidclient.PromptURLParameter) {
|
||||
targetQuery.Set(openidclient.PromptURLParameter, reqQuery.Get(openidclient.PromptURLParameter))
|
||||
}
|
||||
|
||||
target.RawQuery = targetQuery.Encode()
|
||||
|
||||
|
||||
@@ -56,6 +56,52 @@ func TestLogin(t *testing.T) {
|
||||
assert.NotEmpty(t, callbackURL.Query().Get("code"))
|
||||
}
|
||||
|
||||
func TestLoginPrompt(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
// initial login and callback
|
||||
initialSessionCookie := login(t, rpClient, idp)
|
||||
|
||||
// verify session created
|
||||
sess := sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, sess.StatusCode)
|
||||
|
||||
// trigger authorize with prompt=login
|
||||
loginURL, err := url.Parse(idp.RelyingPartyServer.URL + "/oauth2/login?prompt=login")
|
||||
assert.NoError(t, err)
|
||||
loginResp := get(t, rpClient, loginURL.String())
|
||||
assert.Equal(t, http.StatusFound, loginResp.StatusCode)
|
||||
|
||||
cookies := rpClient.Jar.Cookies(loginURL)
|
||||
sessionCookie := getCookieFromJar(cookie.Session, cookies)
|
||||
loginCookie := getCookieFromJar(cookie.Login, cookies)
|
||||
loginLegacyCookie := getCookieFromJar(cookie.LoginLegacy, cookies)
|
||||
|
||||
assert.Nil(t, sessionCookie)
|
||||
assert.NotNil(t, loginCookie)
|
||||
assert.NotNil(t, loginLegacyCookie)
|
||||
|
||||
// verify session deleted
|
||||
sess = sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusUnauthorized, sess.StatusCode)
|
||||
|
||||
// follow redirect to idp
|
||||
authorizeResp := get(t, rpClient, loginResp.Location.String())
|
||||
assert.Equal(t, http.StatusFound, authorizeResp.StatusCode)
|
||||
|
||||
// follow callback back to rp
|
||||
sessionCookie = callback(t, rpClient, authorizeResp)
|
||||
|
||||
// verify new session created
|
||||
sess = sessionInfo(t, idp, rpClient)
|
||||
assert.Equal(t, http.StatusOK, sess.StatusCode)
|
||||
assert.NotEqual(t, initialSessionCookie.Value, sessionCookie.Value)
|
||||
}
|
||||
|
||||
func TestCallback(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
@@ -19,11 +20,14 @@ import (
|
||||
const (
|
||||
LocaleURLParameter = "locale"
|
||||
SecurityLevelURLParameter = "level"
|
||||
PromptURLParameter = "prompt"
|
||||
MaxAgeURLParameter = "max_age"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSecurityLevel = errors.New("InvalidSecurityLevel")
|
||||
ErrInvalidLocale = errors.New("InvalidLocale")
|
||||
ErrInvalidPrompt = errors.New("InvalidPrompt")
|
||||
ErrInvalidLoginParameter = errors.New("InvalidLoginParameter")
|
||||
|
||||
// LoginParameterMapping maps incoming login parameters to OpenID Connect parameters
|
||||
@@ -31,6 +35,8 @@ var (
|
||||
LocaleURLParameter: "ui_locales",
|
||||
SecurityLevelURLParameter: "acr_values",
|
||||
}
|
||||
|
||||
PromptAllowedValues = []string{"login", "select_account"}
|
||||
)
|
||||
|
||||
func NewLogin(c *Client, r *http.Request) (*Login, error) {
|
||||
@@ -49,6 +55,11 @@ func NewLogin(c *Client, r *http.Request) (*Login, error) {
|
||||
return nil, fmt.Errorf("%w: %w", ErrInvalidLocale, err)
|
||||
}
|
||||
|
||||
prompt, err := getPromptParam(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrInvalidPrompt, err)
|
||||
}
|
||||
|
||||
nonce, err := strings.GenerateBase64(32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating nonce: %w", err)
|
||||
@@ -80,8 +91,16 @@ func NewLogin(c *Client, r *http.Request) (*Login, error) {
|
||||
opts = append(opts, oauth2.SetAuthURLParam(LoginParameterMapping[LocaleURLParameter], locale))
|
||||
}
|
||||
|
||||
if len(prompt) > 0 {
|
||||
opts = append(opts, oauth2.SetAuthURLParam(PromptURLParameter, prompt))
|
||||
opts = append(opts, oauth2.SetAuthURLParam(MaxAgeURLParameter, "0"))
|
||||
}
|
||||
|
||||
return &Login{
|
||||
AuthCodeURL: c.oauth2Config.AuthCodeURL(state, opts...),
|
||||
Acr: acr,
|
||||
Locale: locale,
|
||||
Prompt: prompt,
|
||||
LoginCookie: &openid.LoginCookie{
|
||||
Acr: acr,
|
||||
CodeVerifier: codeVerifier,
|
||||
@@ -94,6 +113,9 @@ func NewLogin(c *Client, r *http.Request) (*Login, error) {
|
||||
|
||||
type Login struct {
|
||||
AuthCodeURL string
|
||||
Acr string
|
||||
Locale string
|
||||
Prompt string
|
||||
*openid.LoginCookie
|
||||
}
|
||||
|
||||
@@ -163,3 +185,16 @@ func getLocaleParam(c *Client, r *http.Request) (string, error) {
|
||||
|
||||
return "", fmt.Errorf("%w: invalid value for %s=%s (must be one of '%s')", ErrInvalidLoginParameter, LocaleURLParameter, paramValue, supported)
|
||||
}
|
||||
|
||||
func getPromptParam(r *http.Request) (string, error) {
|
||||
paramValue := r.URL.Query().Get(PromptURLParameter)
|
||||
if len(paramValue) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if slices.Contains(PromptAllowedValues, paramValue) {
|
||||
return paramValue, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: invalid value for %s=%s (must be one of '%s')", ErrInvalidLoginParameter, PromptURLParameter, paramValue, PromptAllowedValues)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ import (
|
||||
|
||||
func TestLogin_URL(t *testing.T) {
|
||||
type loginURLTest struct {
|
||||
name string
|
||||
url string
|
||||
extraParams map[string]string
|
||||
metadata *config.ProviderMetadata
|
||||
error error
|
||||
name string
|
||||
url string
|
||||
wantParams map[string]string
|
||||
metadata *config.ProviderMetadata
|
||||
error error
|
||||
}
|
||||
|
||||
tests := []loginURLTest{
|
||||
@@ -33,7 +33,7 @@ func TestLogin_URL(t *testing.T) {
|
||||
{
|
||||
name: "happy path with level",
|
||||
url: mock.Ingress + "/oauth2/login?level=Level3",
|
||||
extraParams: map[string]string{
|
||||
wantParams: map[string]string{
|
||||
"acr_values": "Level3",
|
||||
},
|
||||
error: nil,
|
||||
@@ -41,15 +41,24 @@ func TestLogin_URL(t *testing.T) {
|
||||
{
|
||||
name: "happy path with locale",
|
||||
url: mock.Ingress + "/oauth2/login?locale=nb",
|
||||
extraParams: map[string]string{
|
||||
wantParams: map[string]string{
|
||||
"ui_locales": "nb",
|
||||
},
|
||||
error: nil,
|
||||
},
|
||||
{
|
||||
name: "happy path with prompt",
|
||||
url: mock.Ingress + "/oauth2/login?prompt=login",
|
||||
wantParams: map[string]string{
|
||||
"prompt": "login",
|
||||
"max_age": "0",
|
||||
},
|
||||
error: nil,
|
||||
},
|
||||
{
|
||||
name: "happy path with both locale and level",
|
||||
url: mock.Ingress + "/oauth2/login?level=Level3&locale=nb",
|
||||
extraParams: map[string]string{
|
||||
wantParams: map[string]string{
|
||||
"acr_values": "Level3",
|
||||
"ui_locales": "nb",
|
||||
},
|
||||
@@ -65,10 +74,15 @@ func TestLogin_URL(t *testing.T) {
|
||||
url: mock.Ingress + "/oauth2/login?locale=es",
|
||||
error: client.ErrInvalidLocale,
|
||||
},
|
||||
{
|
||||
name: "invalid prompt",
|
||||
url: mock.Ingress + "/oauth2/login?prompt=invalid",
|
||||
error: client.ErrInvalidPrompt,
|
||||
},
|
||||
{
|
||||
name: "level idporten-loa-substantial should translate to Level3 for old IDP",
|
||||
url: mock.Ingress + "/oauth2/login?level=idporten-loa-substantial",
|
||||
extraParams: map[string]string{
|
||||
wantParams: map[string]string{
|
||||
"acr_values": "Level3",
|
||||
},
|
||||
error: nil,
|
||||
@@ -76,7 +90,7 @@ func TestLogin_URL(t *testing.T) {
|
||||
{
|
||||
name: "level idporten-loa-high should translate to Level4 for old IDP",
|
||||
url: mock.Ingress + "/oauth2/login?level=idporten-loa-high",
|
||||
extraParams: map[string]string{
|
||||
wantParams: map[string]string{
|
||||
"acr_values": "Level4",
|
||||
},
|
||||
error: nil,
|
||||
@@ -84,7 +98,7 @@ func TestLogin_URL(t *testing.T) {
|
||||
{
|
||||
name: "level Level3 should translate to idporten-loa-substantial for new IDP",
|
||||
url: mock.Ingress + "/oauth2/login?level=Level3",
|
||||
extraParams: map[string]string{
|
||||
wantParams: map[string]string{
|
||||
"acr_values": "idporten-loa-substantial",
|
||||
},
|
||||
metadata: &config.ProviderMetadata{
|
||||
@@ -96,7 +110,7 @@ func TestLogin_URL(t *testing.T) {
|
||||
{
|
||||
name: "level Level4 should translate to idporten-loa-high for new IDP",
|
||||
url: mock.Ingress + "/oauth2/login?level=Level4",
|
||||
extraParams: map[string]string{
|
||||
wantParams: map[string]string{
|
||||
"acr_values": "idporten-loa-high",
|
||||
},
|
||||
metadata: &config.ProviderMetadata{
|
||||
@@ -155,8 +169,8 @@ func TestLogin_URL(t *testing.T) {
|
||||
assert.ElementsMatch(t, query["code_challenge_method"], []string{"S256"})
|
||||
assert.ElementsMatch(t, query["code_challenge"], []string{oauth2.S256ChallengeFromVerifier(result.CodeVerifier)})
|
||||
|
||||
if test.extraParams != nil {
|
||||
for key, value := range test.extraParams {
|
||||
if test.wantParams != nil {
|
||||
for key, value := range test.wantParams {
|
||||
assert.Contains(t, query, key)
|
||||
assert.ElementsMatch(t, query[key], []string{value})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user