Skip to content

Commit

Permalink
oauth2: Adds acr, prompt, max_age to consent flow
Browse files Browse the repository at this point in the history
  • Loading branch information
arekkas committed Dec 10, 2017
1 parent 07519df commit c760f7a
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 29 deletions.
32 changes: 32 additions & 0 deletions oauth2/consent_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,20 @@ type ConsentRequest struct {
// accepted or rejected.
RedirectURL string `json:"redirectUrl"`

RequestedACR []string `json:"requestedAcr"`
RequestedPrompt string `json:"requestedPrompt"`
RequestedMaxAge int64 `json:"requestedMaxAge"`

CSRF string `json:"-"`
GrantedScopes []string `json:"-"`
Subject string `json:"-"`
AccessTokenExtra map[string]interface{} `json:"-"`
IDTokenExtra map[string]interface{} `json:"-"`
Consent string `json:"-"`
DenyReason string `json:"-"`
DenyError string `json:"-"`
AuthTime int64 `json:"-"`
ProvidedACR string `json:"-"`
}

func (c *ConsentRequest) IsConsentGranted() bool {
Expand All @@ -65,6 +72,27 @@ type AcceptConsentRequestPayload struct {

// A list of scopes that the user agreed to grant. It should be a subset of requestedScopes from the consent request.
GrantScopes []string `json:"grantScopes"`

// ProvidedAuthenticationContextClassReference specifies an Authentication Context Class Reference value that identifies
// the Authentication Context Class that the authentication performed satisfied. The value "0" indicates the End-User
// authentication did not meet the requirements of ISO/IEC 29115 [ISO29115] level 1.
//
// In summary ISO/IEC 29115 defines four levels, broadly summarized as follows.
//
// * acr=0 does not satisfy Level 1 and could be, for example, authentication using a long-lived browser cookie.
// * Level 1 (acr=1): Minimal confidence in the asserted identity of the entity, but enough confidence that the
// entity is the same over consecutive authentication events. For example presenting a self-registered
// username or password.
// * Level 2 (acr=2): There is some confidence in the asserted identity of the entity. For example confirming
// authentication using a mobile app ("Something you have").
// * Level 3 (acr=3): High confidence in an asserted identity of the entity. For example sending a code to a mobile
// phone or using Google Authenticator or a fingerprint scanner ("Something you have and something you know" / "Something you are")
// * Level 4 (acr=4): Very high confidence in an asserted identity of the entity. Requires in-person identification.
ProvidedAuthenticationContextClassReference string `json:"providedAcr"`

// AuthTime is the time when the End-User authentication occurred. Its value is a JSON number representing the
// number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.
AuthTime int64 `json:"authTime"`
}

// RejectConsentRequestPayload represents data that will be used to reject a consent request.
Expand All @@ -73,6 +101,10 @@ type AcceptConsentRequestPayload struct {
type RejectConsentRequestPayload struct {
// Reason represents the reason why the user rejected the consent request.
Reason string `json:"reason"`

// Error can be used to return an OpenID Connect or OAuth 2.0 error to the OAuth 2.0 client, such as login_required,
// interaction_required, consent_required.
Error string `json:"error"`
}

type ConsentRequestManager interface {
Expand Down
4 changes: 4 additions & 0 deletions oauth2/consent_manager_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func (m *ConsentRequestMemoryManager) AcceptConsentRequest(id string, payload *A
session.IDTokenExtra = payload.IDTokenExtra
session.Consent = ConsentRequestAccepted
session.GrantedScopes = payload.GrantScopes
session.AuthTime = payload.AuthTime
session.ProvidedACR = payload.ProvidedAuthenticationContextClassReference

return m.PersistConsentRequest(session)
}
Expand All @@ -60,6 +62,8 @@ func (m *ConsentRequestMemoryManager) RejectConsentRequest(id string, payload *R

session.Consent = ConsentRequestRejected
session.DenyReason = payload.Reason
session.DenyError = payload.Error

return m.PersistConsentRequest(session)
}

Expand Down
38 changes: 37 additions & 1 deletion oauth2/consent_manager_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import (
var sqlConsentParams = []string{
"id", "client_id", "expires_at", "redirect_url", "requested_scopes",
"csrf", "granted_scopes", "access_token_extra", "id_token_extra",
"consent", "deny_reason", "subject",
"consent", "deny_reason", "subject", "requested_acr", "requested_prompt",
"provided_acr", "auth_time", "deny_error",
}

var consentMigrations = &migrate.MemoryMigrationSource{
Expand All @@ -56,6 +57,23 @@ var consentMigrations = &migrate.MemoryMigrationSource{
"DROP TABLE hydra_consent_request",
},
},
{
Id: "2",
Up: []string{
"ALTER TABLE hydra_consent_request ADD requested_acr text NOT NULL",
"ALTER TABLE hydra_consent_request ADD provided_acr text NOT NULL",
"ALTER TABLE hydra_consent_request ADD auth_time text NOT NULL",
"ALTER TABLE hydra_consent_request ADD requested_prompt text NOT NULL",
"ALTER TABLE hydra_consent_request ADD deny_error text NOT NULL",
},
Down: []string{
"ALTER TABLE hydra_consent_request DROP COLUMN requested_acr",
"ALTER TABLE hydra_consent_request DROP COLUMN provided_acr",
"ALTER TABLE hydra_consent_request DROP COLUMN auth_time",
"ALTER TABLE hydra_consent_request DROP COLUMN requested_prompt",
"ALTER TABLE hydra_consent_request DROP COLUMN deny_error",
},
},
},
}

Expand All @@ -72,6 +90,11 @@ type consentRequestSqlData struct {
Consent string `db:"consent"`
DenyReason string `db:"deny_reason"`
Subject string `db:"subject"`
RequestedACR string `db:"requested_acr"`
RequestedPrompt string `db:"requested_prompt"`
ProvidedACR string `db:"provided_acr"`
AuthTime int64 `db:"auth_time"`
DenyError string `db:"deny_error"`
}

func newConsentRequestSqlData(request *ConsentRequest) (*consentRequestSqlData, error) {
Expand Down Expand Up @@ -113,7 +136,12 @@ func newConsentRequestSqlData(request *ConsentRequest) (*consentRequestSqlData,
IDTokenExtra: idtext,
Consent: request.Consent,
DenyReason: request.DenyReason,
DenyError: request.DenyError,
Subject: request.Subject,
AuthTime: request.AuthTime,
RequestedACR: strings.Join(request.RequestedACR, " "),
ProvidedACR: request.ProvidedACR,
RequestedPrompt: request.RequestedPrompt,
}, nil
}

Expand All @@ -140,11 +168,16 @@ func (r *consentRequestSqlData) toConsentRequest() (*ConsentRequest, error) {
CSRF: r.CSRF,
Consent: r.Consent,
DenyReason: r.DenyReason,
DenyError: r.DenyError,
RequestedScopes: strings.Split(r.RequestedScopes, " "),
GrantedScopes: strings.Split(r.GrantedScopes, " "),
AccessTokenExtra: atext,
IDTokenExtra: idtext,
Subject: r.Subject,
AuthTime: r.AuthTime,
RequestedACR: strings.Split(r.RequestedACR, " "),
ProvidedACR: r.ProvidedACR,
RequestedPrompt: r.RequestedPrompt,
}, nil
}

Expand Down Expand Up @@ -197,6 +230,8 @@ func (m *ConsentRequestSQLManager) AcceptConsentRequest(id string, payload *Acce
r.IDTokenExtra = payload.IDTokenExtra
r.Consent = ConsentRequestAccepted
r.GrantedScopes = payload.GrantScopes
r.AuthTime = payload.AuthTime
r.ProvidedACR = payload.ProvidedAuthenticationContextClassReference

return m.updateConsentRequest(r)
}
Expand All @@ -209,6 +244,7 @@ func (m *ConsentRequestSQLManager) RejectConsentRequest(id string, payload *Reje

r.Consent = ConsentRequestRejected
r.DenyReason = payload.Reason
r.DenyError = payload.Error

return m.updateConsentRequest(r)
}
Expand Down
7 changes: 6 additions & 1 deletion oauth2/consent_manager_sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@ func TestConsentRequestSqlDataTransforms(t *testing.T) {
RequestedScopes: []string{"foo", "bar"},
GrantedScopes: []string{"baz", "bar"},
CSRF: "some-csrf",
ExpiresAt: time.Now().Round(time.Second),
ExpiresAt: time.Now().UTC().Round(time.Second),
Consent: ConsentRequestAccepted,
DenyReason: "some reason",
AccessTokenExtra: map[string]interface{}{"atfoo": "bar", "atbaz": "bar"},
IDTokenExtra: map[string]interface{}{"idfoo": "bar", "idbaz": "bar"},
RedirectURL: "https://redirect-me/foo",
Subject: "Peter",
RequestedACR: []string{"1", "2"},
RequestedPrompt: "none",
ProvidedACR: "2",
AuthTime: 100,
DenyError: "invalid_login",
},
},
} {
Expand Down
14 changes: 12 additions & 2 deletions oauth2/consent_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,18 @@ func tTestConsentRequestManagerReadWrite(t *testing.T) {
RequestedScopes: []string{"foo", "bar"},
GrantedScopes: []string{"baz", "bar"},
CSRF: "some-csrf",
ExpiresAt: time.Now().Round(time.Minute),
ExpiresAt: time.Now().UTC().Round(time.Minute),
Consent: ConsentRequestAccepted,
DenyReason: "some reason",
AccessTokenExtra: map[string]interface{}{"atfoo": "bar", "atbaz": "bar"},
IDTokenExtra: map[string]interface{}{"idfoo": "bar", "idbaz": "bar"},
RedirectURL: "https://redirect-me/foo",
Subject: "Peter",
RequestedACR: []string{"1", "2"},
RequestedPrompt: "none",
ProvidedACR: "2",
AuthTime: 100,
DenyError: "invalid_login",
}

for k, m := range consentManagers {
Expand All @@ -92,13 +97,18 @@ func TestConsentRequestManagerUpdate(t *testing.T) {
RequestedScopes: []string{"foo", "bar"},
GrantedScopes: []string{"baz", "bar"},
CSRF: "some-csrf",
ExpiresAt: time.Now().Round(time.Minute),
ExpiresAt: time.Now().UTC().Round(time.Minute),
Consent: ConsentRequestRejected,
DenyReason: "some reason",
AccessTokenExtra: map[string]interface{}{"atfoo": "bar", "atbaz": "bar"},
IDTokenExtra: map[string]interface{}{"idfoo": "bar", "idbaz": "bar"},
RedirectURL: "https://redirect-me/foo",
Subject: "Peter",
RequestedACR: []string{"1", "2"},
RequestedPrompt: "none",
ProvidedACR: "2",
AuthTime: 100,
DenyError: "invalid_login",
}

for k, m := range consentManagers {
Expand Down
12 changes: 11 additions & 1 deletion oauth2/consent_sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,18 @@ func TestConsentSDK(t *testing.T) {
RequestedScopes: []string{"foo", "bar"},
GrantedScopes: []string{"baz", "bar"},
CSRF: "some-csrf",
ExpiresAt: time.Now().Round(time.Minute),
ExpiresAt: time.Now().UTC().Round(time.Minute),
Consent: ConsentRequestAccepted,
DenyReason: "some reason",
AccessTokenExtra: map[string]interface{}{"atfoo": "bar", "atbaz": "bar"},
IDTokenExtra: map[string]interface{}{"idfoo": "bar", "idbaz": "bar"},
RedirectURL: "https://redirect-me/foo",
Subject: "Peter",
RequestedACR: []string{"1", "2"},
RequestedPrompt: "none",
ProvidedACR: "2",
AuthTime: 100,
DenyError: "invalid_login",
}

memm := NewConsentRequestMemoryManager()
Expand Down Expand Up @@ -72,12 +77,17 @@ func TestConsentSDK(t *testing.T) {
assert.EqualValues(t, req.ClientID, got.ClientId)
assert.EqualValues(t, req.RequestedScopes, got.RequestedScopes)
assert.EqualValues(t, req.RedirectURL, got.RedirectUrl)
assert.EqualValues(t, req.RequestedPrompt, got.RequestedPrompt)
assert.EqualValues(t, req.RequestedACR, got.RequestedAcr)
assert.EqualValues(t, req.RequestedMaxAge, got.RequestedMaxAge)

accept := hydra.ConsentRequestAcceptance{
Subject: "some-subject",
GrantScopes: []string{"scope1", "scope2"},
AccessTokenExtra: map[string]interface{}{"at": "bar"},
IdTokenExtra: map[string]interface{}{"id": "bar"},
AuthTime: 100,
ProvidedAcr: "1",
}

response, err := client.AcceptOAuth2ConsentRequest(req.ID, accept)
Expand Down
36 changes: 25 additions & 11 deletions oauth2/consent_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
package oauth2

import (
"time"

"net/http"
"strconv"
"strings"
"time"

"github.com/gorilla/sessions"
"github.com/ory/fosite"
Expand Down Expand Up @@ -46,17 +47,21 @@ func (s *DefaultConsentStrategy) ValidateConsentRequest(req fosite.AuthorizeRequ
}

if !consent.IsConsentGranted() {
err := errors.New("The resource owner denied consent for this request")
errorName := "rejected_consent_request"
if consent.DenyError != "" {
errorName = consent.DenyError
}

return nil, &fosite.RFC6749Error{
Name: "rejected_consent_request",
Name: errorName,
Description: consent.DenyReason,
Debug: err.Error(),
Debug: "The consent app denied the authorization request",
Hint: consent.DenyReason,
Code: http.StatusUnauthorized,
}
}

if time.Now().After(consent.ExpiresAt) {
if time.Now().UTC().After(consent.ExpiresAt) {
return nil, errors.Errorf("Token expired")
}

Expand Down Expand Up @@ -88,10 +93,11 @@ func (s *DefaultConsentStrategy) ValidateConsentRequest(req fosite.AuthorizeRequ
Audience: req.GetClient().GetID(),
Subject: consent.Subject,
Issuer: s.Issuer,
IssuedAt: time.Now(),
ExpiresAt: time.Now().Add(s.DefaultIDTokenLifespan),
AuthTime: time.Now(),
Extra: consent.IDTokenExtra,
IssuedAt: time.Now().UTC(),
ExpiresAt: time.Now().UTC().Add(s.DefaultIDTokenLifespan),
AuthTime: time.Unix(consent.AuthTime, 0),
AuthenticationContextClassReference: consent.ProvidedACR,
Extra: consent.IDTokenExtra,
},
// required for lookup on jwk endpoint
Headers: &ejwt.Headers{Extra: map[string]interface{}{"kid": "public"}},
Expand All @@ -105,17 +111,25 @@ func (s *DefaultConsentStrategy) CreateConsentRequest(req fosite.AuthorizeReques
csrf := uuid.New()
id := uuid.New()

maxAge, err := strconv.ParseInt(req.GetRequestForm().Get("max_age"), 10, 64)
if err != nil {
maxAge = 0
}

cookie.Values[CookieCSRFKey] = csrf
consent := &ConsentRequest{
ID: id,
CSRF: csrf,
GrantedScopes: []string{},
RequestedScopes: req.GetRequestedScopes(),
ClientID: req.GetClient().GetID(),
ExpiresAt: time.Now().Add(s.DefaultChallengeLifespan),
ExpiresAt: time.Now().UTC().Add(s.DefaultChallengeLifespan),
RedirectURL: redirectURL + "&consent=" + id,
AccessTokenExtra: map[string]interface{}{},
IDTokenExtra: map[string]interface{}{},
RequestedACR: strings.Split(req.GetRequestForm().Get("acr"), " "),
RequestedPrompt: req.GetRequestForm().Get("prompt"),
RequestedMaxAge: maxAge,
}

if err := s.ConsentManager.PersistConsentRequest(consent); err != nil {
Expand Down

0 comments on commit c760f7a

Please sign in to comment.