Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

consent: Forward session and login information #1013

Merged
merged 1 commit into from
Aug 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions consent/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ func mockConsentRequest(key string, remember bool, rememberFor int, hasError boo
CSRF: "csrf" + key,
ForceSubjectIdentifier: "forced-subject",
SubjectIdentifier: "forced-subject",
LoginSessionID: "login-session-id",
LoginChallenge: "login-challenge",
}

var err *RequestDeniedError
Expand Down Expand Up @@ -112,6 +114,7 @@ func mockAuthRequest(key string, authAt bool) (c *AuthenticationRequest, h *Hand
RequestedScope: []string{"scopea" + key, "scopeb" + key},
Verifier: "verifier" + key,
CSRF: "csrf" + key,
SessionID: "login-session-id",
}

var err = &RequestDeniedError{
Expand Down
23 changes: 22 additions & 1 deletion consent/sql_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,19 @@ var migrations = &migrate.MemoryMigrationSource{
"DROP TABLE hydra_oauth2_obfuscated_authentication_session",
},
},
{
Id: "3",
Up: []string{
`ALTER TABLE hydra_oauth2_consent_request ADD login_session_id VARCHAR(40) NULL DEFAULT ''`,
`ALTER TABLE hydra_oauth2_consent_request ADD login_challenge VARCHAR(40) NULL DEFAULT ''`,
`ALTER TABLE hydra_oauth2_authentication_request ADD login_session_id VARCHAR(40) NULL DEFAULT ''`,
},
Down: []string{
`ALTER TABLE hydra_oauth2_consent_request DROP COLUMN login_session_id`,
`ALTER TABLE hydra_oauth2_consent_request DROP COLUMN login_challenge`,
`ALTER TABLE hydra_oauth2_authentication_request DROP COLUMN login_session_id`,
},
},
},
}

Expand Down Expand Up @@ -151,9 +164,10 @@ var sqlParamsAuthenticationRequest = []string{
"requested_at",
"csrf",
"oidc_context",
"login_session_id",
}

var sqlParamsConsentRequest = append(sqlParamsAuthenticationRequest, "forced_subject_identifier")
var sqlParamsConsentRequest = append(sqlParamsAuthenticationRequest, "forced_subject_identifier", "login_challenge")

var sqlParamsConsentRequestHandled = []string{
"challenge",
Expand Down Expand Up @@ -186,10 +200,13 @@ type sqlAuthenticationRequest struct {
CSRF string `db:"csrf"`
AuthenticatedAt *time.Time `db:"authenticated_at"`
RequestedAt time.Time `db:"requested_at"`
SessionID string `db:"login_session_id"`
}

type sqlConsentRequest struct {
sqlAuthenticationRequest
LoginChallenge string `db:"login_challenge"`
LoginSessionID string `db:"login_session_id"`
ForcedSubjectIdentifier string `db:"forced_subject_identifier"`
}

Expand Down Expand Up @@ -226,7 +243,10 @@ func newSQLConsentRequest(c *ConsentRequest) (*sqlConsentRequest, error) {
CSRF: c.CSRF,
AuthenticatedAt: toMySQLDateHack(c.AuthenticatedAt),
RequestedAt: c.RequestedAt,
SessionID: c.LoginSessionID,
},
LoginSessionID: c.LoginSessionID,
LoginChallenge: c.LoginChallenge,
ForcedSubjectIdentifier: c.ForceSubjectIdentifier,
}, nil
}
Expand All @@ -249,6 +269,7 @@ func newSQLAuthenticationRequest(c *AuthenticationRequest) (*sqlAuthenticationRe
CSRF: c.CSRF,
AuthenticatedAt: toMySQLDateHack(c.AuthenticatedAt),
RequestedAt: c.RequestedAt,
SessionID: c.SessionID,
}, nil
}

Expand Down
24 changes: 16 additions & 8 deletions consent/strategy_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,24 +102,24 @@ var ErrNoPreviousConsentFound = errors.New("No previous OAuth 2.0 Consent could
func (s *DefaultStrategy) requestAuthentication(w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester) error {
prompt := stringsx.Splitx(ar.GetRequestForm().Get("prompt"), " ")
if stringslice.Has(prompt, "login") {
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{})
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{}, nil)
}

// We try to open the session cookie. If it does not exist (indicated by the error), we must authenticate the user.
cookie, err := s.CookieStore.Get(r, cookieAuthenticationName)
if err != nil {
//id.L.WithError(err).Debug("No OAuth2 authentication session was found, performing consent authentication flow")
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{})
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{}, nil)
}

sessionID := mapx.GetStringDefault(cookie.Values, cookieAuthenticationSIDName, "")
if sessionID == "" {
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{})
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{}, nil)
}

session, err := s.M.GetAuthenticationSession(sessionID)
if errors.Cause(err) == pkg.ErrNotFound {
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{})
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{}, nil)
} else if err != nil {
return err
}
Expand All @@ -137,12 +137,12 @@ func (s *DefaultStrategy) requestAuthentication(w http.ResponseWriter, r *http.R
if stringslice.Has(prompt, "none") {
return errors.WithStack(fosite.ErrLoginRequired.WithDebug("Request failed because prompt is set to \"none\" and authentication time reached max_age"))
}
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{})
return s.forwardAuthenticationRequest(w, r, ar, "", time.Time{}, nil)
}

idTokenHint := ar.GetRequestForm().Get("id_token_hint")
if idTokenHint == "" {
return s.forwardAuthenticationRequest(w, r, ar, session.Subject, session.AuthenticatedAt)
return s.forwardAuthenticationRequest(w, r, ar, session.Subject, session.AuthenticatedAt, session)
}

token, err := s.JWTStrategy.Decode(idTokenHint)
Expand Down Expand Up @@ -170,11 +170,11 @@ func (s *DefaultStrategy) requestAuthentication(w http.ResponseWriter, r *http.R
if hintSub != session.Subject && hintSub != obfuscatedUserID && hintSub != forcedObfuscatedUserID {
return errors.WithStack(fosite.ErrLoginRequired.WithDebug("Request failed because subject claim from id_token_hint does not match subject from authentication session"))
} else {
return s.forwardAuthenticationRequest(w, r, ar, session.Subject, session.AuthenticatedAt)
return s.forwardAuthenticationRequest(w, r, ar, session.Subject, session.AuthenticatedAt, session)
}
}

func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester, subject string, authenticatedAt time.Time) error {
func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r *http.Request, ar fosite.AuthorizeRequester, subject string, authenticatedAt time.Time, session *AuthenticationSession) error {
if (subject != "" && authenticatedAt.IsZero()) || (subject == "" && !authenticatedAt.IsZero()) {
return errors.WithStack(fosite.ErrServerError.WithDebug("Consent strategy returned a non-empty subject with an empty auth date, or an empty subject with a non-empty auth date"))
}
Expand Down Expand Up @@ -212,6 +212,11 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r
}
}

sessionID := ""
if session != nil {
sessionID = session.ID
}

// Set the session
if err := s.M.CreateAuthenticationRequest(
&AuthenticationRequest{
Expand All @@ -225,6 +230,7 @@ func (s *DefaultStrategy) forwardAuthenticationRequest(w http.ResponseWriter, r
RequestURL: iu.String(),
AuthenticatedAt: authenticatedAt,
RequestedAt: time.Now().UTC(),
SessionID: sessionID,
OpenIDConnectContext: &OpenIDConnectContext{
IDTokenHintClaims: idTokenHintClaims,
ACRValues: stringsx.Splitx(ar.GetRequestForm().Get("acr_values"), " "),
Expand Down Expand Up @@ -505,6 +511,8 @@ func (s *DefaultStrategy) forwardConsentRequest(w http.ResponseWriter, r *http.R
RequestedAt: as.RequestedAt,
ForceSubjectIdentifier: as.ForceSubjectIdentifier,
OpenIDConnectContext: as.AuthenticationRequest.OpenIDConnectContext,
LoginSessionID: as.AuthenticationRequest.SessionID,
LoginChallenge: as.AuthenticationRequest.Challenge,
},
); err != nil {
return errors.WithStack(err)
Expand Down
62 changes: 62 additions & 0 deletions consent/strategy_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ func TestStrategy(t *testing.T) {
assert.Contains(t, lr.RequestUrl, "/oauth2/auth?login_verifier=&consent_verifier=&")
assert.EqualValues(t, false, lr.Skip)
assert.EqualValues(t, "user", lr.Subject)
assert.NotEmpty(t, lr.LoginChallenge)
assert.Empty(t, lr.LoginSessionId)
assert.EqualValues(t, swagger.OpenIdConnectContext{AcrValues: []string{"1", "2"}, Display: "page", UiLocales: []string{"de", "en"}}, lr.OidcContext)
w.WriteHeader(http.StatusNoContent)
}
Expand Down Expand Up @@ -346,6 +348,66 @@ func TestStrategy(t *testing.T) {
},
},
},
{
d: "This should pass because login was remembered and session id should be set",
req: fosite.AuthorizeRequest{Request: fosite.Request{Client: &client.Client{ClientID: "client-id"}, Scopes: []string{"scope-a"}}},
jar: persistentCJ,
lph: func(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
lr, res, err := apiClient.GetLoginRequest(r.URL.Query().Get("login_challenge"))
require.NoError(t, err)
require.EqualValues(t, http.StatusOK, res.StatusCode)
assert.True(t, lr.Skip)
assert.NotEmpty(t, lr.SessionId)
v, res, err := apiClient.AcceptLoginRequest(r.URL.Query().Get("login_challenge"), swagger.AcceptLoginRequest{
Subject: "user",
Remember: false,
RememberFor: 0,
Acr: "1",
})
require.NoError(t, err)
require.EqualValues(t, http.StatusOK, res.StatusCode)
require.NotEmpty(t, v.RedirectTo)
http.Redirect(w, r, v.RedirectTo, http.StatusFound)
}
},
cph: func(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
cr, res, err := apiClient.GetConsentRequest(r.URL.Query().Get("consent_challenge"))
require.NoError(t, err)
require.EqualValues(t, http.StatusOK, res.StatusCode)
assert.True(t, cr.Skip)
assert.NotEmpty(t, cr.LoginSessionId)
assert.NotEmpty(t, cr.LoginChallenge)
v, res, err := apiClient.AcceptConsentRequest(r.URL.Query().Get("consent_challenge"), swagger.AcceptConsentRequest{
GrantScope: []string{"scope-a"},
Remember: false,
RememberFor: 0,
Session: swagger.ConsentRequestSession{
AccessToken: map[string]interface{}{"foo": "bar"},
IdToken: map[string]interface{}{"bar": "baz"},
},
})
require.NoError(t, err)
require.EqualValues(t, http.StatusOK, res.StatusCode)
require.NotEmpty(t, v.RedirectTo)
http.Redirect(w, r, v.RedirectTo, http.StatusFound)
}
},
expectFinalStatusCode: http.StatusOK,
expectErrType: []error{ErrAbortOAuth2Request, ErrAbortOAuth2Request, nil},
expectErr: []bool{true, true, false},
expectSession: &HandledConsentRequest{
ConsentRequest: &ConsentRequest{Subject: "user", SubjectIdentifier: "user"},
GrantedScope: []string{"scope-a"},
Remember: false,
RememberFor: 0,
Session: &ConsentRequestSessionData{
AccessToken: map[string]interface{}{"foo": "bar"},
IDToken: map[string]interface{}{"bar": "baz"},
},
},
},
{
d: "This should fail because prompt=none, client is public, and redirection scheme is not HTTPS but a custom scheme",
req: fosite.AuthorizeRequest{RedirectURI: mustParseURL(t, "custom://redirection-scheme/path"), Request: fosite.Request{Client: &client.Client{TokenEndpointAuthMethod: "none", ClientID: "client-id"}, Scopes: []string{"scope-a"}}},
Expand Down
12 changes: 12 additions & 0 deletions consent/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ type AuthenticationRequest struct {
// might come in handy if you want to deal with additional request parameters.
RequestURL string `json:"request_url"`

// SessionID is the authentication session ID. It is set if the browser had a valid authentication session at
// ORY Hydra during the login flow. It can be used to associate consecutive login requests by a certain user.
SessionID string `json:"session_id"`

ForceSubjectIdentifier string `json:"-"` // this is here but has no meaning apart from sql_helper working properly.
Verifier string `json:"-"`
CSRF string `json:"-"`
Expand Down Expand Up @@ -292,6 +296,14 @@ type ConsentRequest struct {
// might come in handy if you want to deal with additional request parameters.
RequestURL string `json:"request_url"`

// LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate
// a login and consent request in the login & consent app.
LoginChallenge string `json:"login_challenge"`

// LoginSessionID is the authentication session ID. It is set if the browser had a valid authentication session at
// ORY Hydra during the login flow. It can be used to associate consecutive login requests by a certain user.
LoginSessionID string `json:"login_session_id"`

// ForceSubjectIdentifier is the value from authentication (if set).
ForceSubjectIdentifier string `json:"-"`
SubjectIdentifier string `json:"-"`
Expand Down
23 changes: 23 additions & 0 deletions docs/api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,16 @@
"client": {
"$ref": "#/definitions/oAuth2Client"
},
"login_challenge": {
"description": "LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate\na login and consent request in the login \u0026 consent app.",
"type": "string",
"x-go-name": "LoginChallenge"
},
"login_session_id": {
"description": "LoginSessionID is the authentication session ID. It is set if the browser had a valid authentication session at\nORY Hydra during the login flow. It can be used to associate consecutive login requests by a certain user.",
"type": "string",
"x-go-name": "LoginSessionID"
},
"oidc_context": {
"$ref": "#/definitions/openIDConnectContext"
},
Expand Down Expand Up @@ -2371,6 +2381,11 @@
},
"x-go-name": "RequestedScope"
},
"session_id": {
"description": "SessionID is the authentication session ID. It is set if the browser had a valid authentication session at\nORY Hydra during the login flow. It can be used to associate consecutive login requests by a certain user.",
"type": "string",
"x-go-name": "SessionID"
},
"skip": {
"description": "Skip, if true, implies that the client has requested the same scopes from the same user previously.\nIf true, you can skip asking the user to grant the requested scopes, and simply forward the user to the redirect URL.\n\nThis feature allows you to update / set session information.",
"type": "boolean",
Expand All @@ -2389,6 +2404,14 @@
"type": "object",
"title": "Client represents an OAuth 2.0 Client.",
"properties": {
"allowed_cors_origins": {
"description": "AllowedCORSOrigins are one or more URLs (scheme://host[:port]) which are allowed to make CORS requests\nto the /oauth/token endpoint. If this array is empty, the sever's CORS origin configuration (`CORS_ALLOWED_ORIGINS`)\nwill be used instead. If this array is set, the allowed origins are appended to the server's CORS origin configuration.\nBe aware that environment variable `CORS_ENABLED` MUST be set to `true` for this to work.",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "AllowedCORSOrigins"
},
"client_id": {
"description": "ClientID is the id for this client.",
"type": "string",
Expand Down
6 changes: 6 additions & 0 deletions sdk/go/hydra/swagger/consent_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ type ConsentRequest struct {

Client OAuth2Client `json:"client,omitempty"`

// LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate a login and consent request in the login & consent app.
LoginChallenge string `json:"login_challenge,omitempty"`

// LoginSessionID is the authentication session ID. It is set if the browser had a valid authentication session at ORY Hydra during the login flow. It can be used to associate consecutive login requests by a certain user.
LoginSessionId string `json:"login_session_id,omitempty"`

OidcContext OpenIdConnectContext `json:"oidc_context,omitempty"`

// RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but might come in handy if you want to deal with additional request parameters.
Expand Down
2 changes: 2 additions & 0 deletions sdk/go/hydra/swagger/docs/ConsentRequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**Challenge** | **string** | Challenge is the identifier (\"authorization challenge\") of the consent authorization request. It is used to identify the session. | [optional] [default to null]
**Client** | [**OAuth2Client**](oAuth2Client.md) | | [optional] [default to null]
**LoginChallenge** | **string** | LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate a login and consent request in the login & consent app. | [optional] [default to null]
**LoginSessionId** | **string** | LoginSessionID is the authentication session ID. It is set if the browser had a valid authentication session at ORY Hydra during the login flow. It can be used to associate consecutive login requests by a certain user. | [optional] [default to null]
**OidcContext** | [**OpenIdConnectContext**](openIDConnectContext.md) | | [optional] [default to null]
**RequestUrl** | **string** | RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but might come in handy if you want to deal with additional request parameters. | [optional] [default to null]
**RequestedScope** | **[]string** | RequestedScope contains all scopes requested by the OAuth 2.0 client. | [optional] [default to null]
Expand Down
1 change: 1 addition & 0 deletions sdk/go/hydra/swagger/docs/LoginRequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Name | Type | Description | Notes
**OidcContext** | [**OpenIdConnectContext**](openIDConnectContext.md) | | [optional] [default to null]
**RequestUrl** | **string** | RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but might come in handy if you want to deal with additional request parameters. | [optional] [default to null]
**RequestedScope** | **[]string** | RequestedScope contains all scopes requested by the OAuth 2.0 client. | [optional] [default to null]
**SessionId** | **string** | SessionID is the authentication session ID. It is set if the browser had a valid authentication session at ORY Hydra during the login flow. It can be used to associate consecutive login requests by a certain user. | [optional] [default to null]
**Skip** | **bool** | Skip, if true, implies that the client has requested the same scopes from the same user previously. If true, you can skip asking the user to grant the requested scopes, and simply forward the user to the redirect URL. This feature allows you to update / set session information. | [optional] [default to null]
**Subject** | **string** | Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope requested by the OAuth 2.0 client. If this value is set and `skip` is true, you MUST include this subject type when accepting the login request, or the request will fail. | [optional] [default to null]

Expand Down
Loading