feat(handler/login): add support for prompt param in login

This commit is contained in:
Trong Huu Nguyen
2023-12-19 08:46:01 +01:00
parent 8f3c5cde88
commit a10da5d0d7
5 changed files with 139 additions and 15 deletions
+27 -1
View File
@@ -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)
}
+3
View File
@@ -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()
+46
View File
@@ -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)
+35
View File
@@ -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)
}
+28 -14
View File
@@ -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})
}