forked from mystiq/dex
Merge pull request #103 from yifan-gu/offline
return refresh token only when scope contains 'offline_access'
This commit is contained in:
commit
081bfdd13d
14 changed files with 195 additions and 82 deletions
|
@ -11,30 +11,22 @@ Sec. 2. [ID Token](http://openid.net/specs/openid-connect-core-1_0.html#IDToken)
|
||||||
- None of the OPTIONAL claims (`acr`, `amr`, `azp`, `auth_time`) are supported
|
- None of the OPTIONAL claims (`acr`, `amr`, `azp`, `auth_time`) are supported
|
||||||
- dex signs using JWS but does not do the OPTIONAL encryption.
|
- dex signs using JWS but does not do the OPTIONAL encryption.
|
||||||
|
|
||||||
|
|
||||||
Sec. 3. [Authentication](http://openid.net/specs/openid-connect-core-1_0.html#Authentication)
|
Sec. 3. [Authentication](http://openid.net/specs/openid-connect-core-1_0.html#Authentication)
|
||||||
- Only the authorization code flow (where `response_type` is `code`) is supported.
|
- Only the authorization code flow (where `response_type` is `code`) is supported.
|
||||||
|
|
||||||
Sec. 3.1.2. [Authorization Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint)
|
Sec. 3.1.2. [Authorization Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint)
|
||||||
- In a production system TLS is required but the dex web-server only supports HTTP right now - it is expected that until HTTPS is supported, TLS termination will be handled outside of dex.
|
- In a production system TLS is required but the dex web-server only supports HTTP right now - it is expected that until HTTPS is supported, TLS termination will be handled outside of dex.
|
||||||
|
|
||||||
|
|
||||||
Sec. 3.1.2.1. [Authentication Request](http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest)
|
Sec. 3.1.2.1. [Authentication Request](http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest)
|
||||||
- dex doesn't check the value of "scope" to make sure it contains "openid" in authentication requests.
|
|
||||||
- max_age not implemented; it's OPTIONAL in the spec, but if it's present servers MUST include auth_time, which dex does not.
|
- max_age not implemented; it's OPTIONAL in the spec, but if it's present servers MUST include auth_time, which dex does not.
|
||||||
- None of the other OPTIONAL parameters are implemented with the exception of:
|
- None of the other OPTIONAL parameters are implemented with the exception of:
|
||||||
- state
|
- state
|
||||||
- nonce
|
- nonce
|
||||||
- dex also defines a non-standard `register` parameter; when this parameter is `1`, end-users are taken through a registration flow, which after completing successfully, lands them at the specified `redirect_uri`
|
- dex also defines a non-standard `register` parameter; when this parameter is `1`, end-users are taken through a registration flow, which after completing successfully, lands them at the specified `redirect_uri`
|
||||||
|
|
||||||
Sec. 3.1.2.2. [Authentication Request Validation](http://openid.net/specs/openid-connect-core-1_0.html#AuthRequestValidation)
|
|
||||||
- As mentioned earlier, dex doesn't validate that the `openid` scope value is present.
|
|
||||||
|
|
||||||
|
|
||||||
Sec. 3.2.2.3. [Authorization Server Authenticates End-User](http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthenticates)
|
Sec. 3.2.2.3. [Authorization Server Authenticates End-User](http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthenticates)
|
||||||
- The spec states that the authentication server "MUST NOT interact with the End-User" when `prompt` is `none` We don't check the prompt parameter at all; similarly, dex MUST re-prompt when `prompt` is `login` - dex does not do this either.
|
- The spec states that the authentication server "MUST NOT interact with the End-User" when `prompt` is `none` We don't check the prompt parameter at all; similarly, dex MUST re-prompt when `prompt` is `login` - dex does not do this either.
|
||||||
|
|
||||||
|
|
||||||
Sec. 3.1.3.2. [Token Request Validation](http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation)
|
Sec. 3.1.3.2. [Token Request Validation](http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation)
|
||||||
- In Token requests, dex chooses to proceed without error when `redirect_uri` is not present and there's only one registered valid URI (which is valid behavior)
|
- In Token requests, dex chooses to proceed without error when `redirect_uri` is not present and there's only one registered valid URI (which is valid behavior)
|
||||||
|
|
||||||
|
@ -64,8 +56,8 @@ Sec. 9. [Client Authentication](http://openid.net/specs/openid-connect-core-1_0.
|
||||||
- dex only supports the `client_secret_basic` client authentication type.
|
- dex only supports the `client_secret_basic` client authentication type.
|
||||||
|
|
||||||
Sec. 11. [Offline Access](http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess)
|
Sec. 11. [Offline Access](http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess)
|
||||||
- dex does not implement this feature.
|
- offline_access in 'scope' is supported, but as we haven't implemented 'prompt' yet, the
|
||||||
|
spec's requirement is not fully met yet.
|
||||||
|
|
||||||
Sec. 15.1. [Mandatory to Implement Features for All OpenID Providers](http://openid.net/specs/openid-connect-core-1_0.html#ImplementationConsiderations)
|
Sec. 15.1. [Mandatory to Implement Features for All OpenID Providers](http://openid.net/specs/openid-connect-core-1_0.html#ImplementationConsiderations)
|
||||||
- dex is missing the follow mandatory features (some are already noted elsewhere in this document):
|
- dex is missing the follow mandatory features (some are already noted elsewhere in this document):
|
||||||
|
|
2
db/migrations/0007_session_scope.sql
Normal file
2
db/migrations/0007_session_scope.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- +migrate Up
|
||||||
|
ALTER TABLE session ADD COLUMN "scope" text;
|
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-gorp/gorp"
|
"github.com/go-gorp/gorp"
|
||||||
|
@ -42,6 +43,7 @@ type sessionModel struct {
|
||||||
UserID string `db:"user_id"`
|
UserID string `db:"user_id"`
|
||||||
Register bool `db:"register"`
|
Register bool `db:"register"`
|
||||||
Nonce string `db:"nonce"`
|
Nonce string `db:"nonce"`
|
||||||
|
Scope string `db:"scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sessionModel) session() (*session.Session, error) {
|
func (s *sessionModel) session() (*session.Session, error) {
|
||||||
|
@ -71,6 +73,7 @@ func (s *sessionModel) session() (*session.Session, error) {
|
||||||
UserID: s.UserID,
|
UserID: s.UserID,
|
||||||
Register: s.Register,
|
Register: s.Register,
|
||||||
Nonce: s.Nonce,
|
Nonce: s.Nonce,
|
||||||
|
Scope: strings.Fields(s.Scope),
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.CreatedAt != 0 {
|
if s.CreatedAt != 0 {
|
||||||
|
@ -101,6 +104,7 @@ func newSessionModel(s *session.Session) (*sessionModel, error) {
|
||||||
UserID: s.UserID,
|
UserID: s.UserID,
|
||||||
Register: s.Register,
|
Register: s.Register,
|
||||||
Nonce: s.Nonce,
|
Nonce: s.Nonce,
|
||||||
|
Scope: strings.Join(s.Scope, " "),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.CreatedAt.IsZero() {
|
if !s.CreatedAt.IsZero() {
|
||||||
|
|
|
@ -196,7 +196,7 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) {
|
||||||
|
|
||||||
// this will actually happen due to some interaction between the
|
// this will actually happen due to some interaction between the
|
||||||
// end-user and a remote identity provider
|
// end-user and a remote identity provider
|
||||||
sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false)
|
sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false, []string{"openid", "offline_access"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,9 +330,37 @@ func handleAuthFunc(srv OIDCServer, idpcs []connector.Connector, tpl *template.T
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check scopes.
|
||||||
|
var scopes []string
|
||||||
|
foundOpenIDScope := false
|
||||||
|
for _, scope := range acr.Scope {
|
||||||
|
switch scope {
|
||||||
|
case "openid":
|
||||||
|
foundOpenIDScope = true
|
||||||
|
scopes = append(scopes, scope)
|
||||||
|
case "offline_access":
|
||||||
|
// According to the spec, for offline_access scope, the client must
|
||||||
|
// use a response_type value that would result in an Authorization Code.
|
||||||
|
// Currently oauth2.ResponseTypeCode is the only supported response type,
|
||||||
|
// and it's been checked above, so we don't need to check it again here.
|
||||||
|
//
|
||||||
|
// TODO(yifan): Verify that 'consent' should be in 'prompt'.
|
||||||
|
scopes = append(scopes, scope)
|
||||||
|
default:
|
||||||
|
// Pass all other scopes.
|
||||||
|
scopes = append(scopes, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundOpenIDScope {
|
||||||
|
log.Errorf("Invalid auth request: missing 'openid' in 'scope'")
|
||||||
|
writeAuthError(w, oauth2.NewError(oauth2.ErrorInvalidRequest), acr.State)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
nonce := q.Get("nonce")
|
nonce := q.Get("nonce")
|
||||||
|
|
||||||
key, err := srv.NewSession(connectorID, acr.ClientID, acr.State, redirectURL, nonce, register)
|
key, err := srv.NewSession(connectorID, acr.ClientID, acr.State, redirectURL, nonce, register, acr.Scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Error creating new session: %v: ", err)
|
log.Errorf("Error creating new session: %v: ", err)
|
||||||
redirectAuthError(w, err, acr.State, redirectURL)
|
redirectAuthError(w, err, acr.State, redirectURL)
|
||||||
|
|
|
@ -102,6 +102,7 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
|
||||||
"response_type": []string{"code"},
|
"response_type": []string{"code"},
|
||||||
"client_id": []string{"XXX"},
|
"client_id": []string{"XXX"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusTemporaryRedirect,
|
wantCode: http.StatusTemporaryRedirect,
|
||||||
wantLocation: "http://fake.example.com",
|
wantLocation: "http://fake.example.com",
|
||||||
|
@ -114,6 +115,7 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
|
||||||
"redirect_uri": []string{"http://client.example.com/callback"},
|
"redirect_uri": []string{"http://client.example.com/callback"},
|
||||||
"client_id": []string{"XXX"},
|
"client_id": []string{"XXX"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusTemporaryRedirect,
|
wantCode: http.StatusTemporaryRedirect,
|
||||||
wantLocation: "http://fake.example.com",
|
wantLocation: "http://fake.example.com",
|
||||||
|
@ -126,6 +128,7 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
|
||||||
"redirect_uri": []string{"http://unrecognized.example.com/callback"},
|
"redirect_uri": []string{"http://unrecognized.example.com/callback"},
|
||||||
"client_id": []string{"XXX"},
|
"client_id": []string{"XXX"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusBadRequest,
|
wantCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
@ -137,6 +140,7 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
|
||||||
"redirect_uri": []string{"http://client.example.com/callback"},
|
"redirect_uri": []string{"http://client.example.com/callback"},
|
||||||
"client_id": []string{"YYY"},
|
"client_id": []string{"YYY"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusBadRequest,
|
wantCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
@ -147,10 +151,22 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
|
||||||
"response_type": []string{"token"},
|
"response_type": []string{"token"},
|
||||||
"client_id": []string{"XXX"},
|
"client_id": []string{"XXX"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusTemporaryRedirect,
|
wantCode: http.StatusTemporaryRedirect,
|
||||||
wantLocation: "http://client.example.com/callback?error=unsupported_response_type&state=",
|
wantLocation: "http://client.example.com/callback?error=unsupported_response_type&state=",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// no 'openid' in scope
|
||||||
|
{
|
||||||
|
query: url.Values{
|
||||||
|
"response_type": []string{"code"},
|
||||||
|
"redirect_uri": []string{"http://client.example.com/callback"},
|
||||||
|
"client_id": []string{"XXX"},
|
||||||
|
"connector_id": []string{"fake"},
|
||||||
|
},
|
||||||
|
wantCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
|
@ -211,6 +227,7 @@ func TestHandleAuthFuncResponsesMultipleRedirectURLs(t *testing.T) {
|
||||||
"redirect_uri": []string{"http://foo.example.com/callback"},
|
"redirect_uri": []string{"http://foo.example.com/callback"},
|
||||||
"client_id": []string{"XXX"},
|
"client_id": []string{"XXX"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusTemporaryRedirect,
|
wantCode: http.StatusTemporaryRedirect,
|
||||||
wantLocation: "http://fake.example.com",
|
wantLocation: "http://fake.example.com",
|
||||||
|
@ -223,6 +240,7 @@ func TestHandleAuthFuncResponsesMultipleRedirectURLs(t *testing.T) {
|
||||||
"redirect_uri": []string{"http://bar.example.com/callback"},
|
"redirect_uri": []string{"http://bar.example.com/callback"},
|
||||||
"client_id": []string{"XXX"},
|
"client_id": []string{"XXX"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusTemporaryRedirect,
|
wantCode: http.StatusTemporaryRedirect,
|
||||||
wantLocation: "http://fake.example.com",
|
wantLocation: "http://fake.example.com",
|
||||||
|
@ -235,6 +253,7 @@ func TestHandleAuthFuncResponsesMultipleRedirectURLs(t *testing.T) {
|
||||||
"redirect_uri": []string{"http://unrecognized.example.com/callback"},
|
"redirect_uri": []string{"http://unrecognized.example.com/callback"},
|
||||||
"client_id": []string{"XXX"},
|
"client_id": []string{"XXX"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusBadRequest,
|
wantCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
@ -245,6 +264,7 @@ func TestHandleAuthFuncResponsesMultipleRedirectURLs(t *testing.T) {
|
||||||
"response_type": []string{"code"},
|
"response_type": []string{"code"},
|
||||||
"client_id": []string{"XXX"},
|
"client_id": []string{"XXX"},
|
||||||
"connector_id": []string{"fake"},
|
"connector_id": []string{"fake"},
|
||||||
|
"scope": []string{"openid"},
|
||||||
},
|
},
|
||||||
wantCode: http.StatusBadRequest,
|
wantCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
|
|
@ -245,7 +245,7 @@ func TestSendResetPasswordEmailHandler(t *testing.T) {
|
||||||
t.Fatalf("case %d: could not make test fixtures: %v", i, err)
|
t.Fatalf("case %d: could not make test fixtures: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = f.srv.NewSession("local", "XXX", "", f.redirectURL, "", true)
|
_, err = f.srv.NewSession("local", "XXX", "", f.redirectURL, "", true, []string{"openid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("case %d: could not create new session: %v", i, err)
|
t.Fatalf("case %d: could not create new session: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,7 +197,7 @@ func TestHandleRegister(t *testing.T) {
|
||||||
t.Fatalf("case %d: could not make test fixtures: %v", i, err)
|
t.Fatalf("case %d: could not make test fixtures: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := f.srv.NewSession(tt.connID, "XXX", "", f.redirectURL, "", true)
|
key, err := f.srv.NewSession(tt.connID, "XXX", "", f.redirectURL, "", true, []string{"openid"})
|
||||||
t.Logf("case %d: key for NewSession: %v", i, key)
|
t.Logf("case %d: key for NewSession: %v", i, key)
|
||||||
|
|
||||||
if tt.attachRemote {
|
if tt.attachRemote {
|
||||||
|
|
|
@ -39,7 +39,7 @@ const (
|
||||||
|
|
||||||
type OIDCServer interface {
|
type OIDCServer interface {
|
||||||
ClientMetadata(string) (*oidc.ClientMetadata, error)
|
ClientMetadata(string) (*oidc.ClientMetadata, error)
|
||||||
NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool) (string, error)
|
NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error)
|
||||||
Login(oidc.Identity, string) (string, error)
|
Login(oidc.Identity, string) (string, error)
|
||||||
// CodeToken exchanges a code for an ID token and a refresh token string on success.
|
// CodeToken exchanges a code for an ID token and a refresh token string on success.
|
||||||
CodeToken(creds oidc.ClientCredentials, sessionKey string) (*jose.JWT, string, error)
|
CodeToken(creds oidc.ClientCredentials, sessionKey string) (*jose.JWT, string, error)
|
||||||
|
@ -263,8 +263,8 @@ func (s *Server) ClientMetadata(clientID string) (*oidc.ClientMetadata, error) {
|
||||||
return s.ClientIdentityRepo.Metadata(clientID)
|
return s.ClientIdentityRepo.Metadata(clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) NewSession(ipdcID, clientID, clientState string, redirectURL url.URL, nonce string, register bool) (string, error) {
|
func (s *Server) NewSession(ipdcID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error) {
|
||||||
sessionID, err := s.SessionManager.NewSession(ipdcID, clientID, clientState, redirectURL, nonce, register)
|
sessionID, err := s.SessionManager.NewSession(ipdcID, clientID, clientState, redirectURL, nonce, register, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -422,20 +422,26 @@ func (s *Server) CodeToken(creds oidc.ClientCredentials, sessionKey string) (*jo
|
||||||
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Session %s token sent: clientID=%s", sessionID, creds.ID)
|
// Generate refresh token when 'scope' contains 'offline_access'.
|
||||||
|
var refreshToken string
|
||||||
|
|
||||||
// Generate refresh token.
|
for _, scope := range ses.Scope {
|
||||||
//
|
if scope == "offline_access" {
|
||||||
// TODO(yifan): Return refresh token only when 'access_type == offline',
|
log.Infof("Session %s requests offline access, will generate refresh token", sessionID)
|
||||||
// or 'scope' == 'offline_access'.
|
|
||||||
refreshToken, err := s.RefreshTokenRepo.Create(ses.UserID, creds.ID)
|
refreshToken, err = s.RefreshTokenRepo.Create(ses.UserID, creds.ID)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
log.Errorf("Failed to generate refresh token: %v", err)
|
log.Errorf("Failed to generate refresh token: %v", err)
|
||||||
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
return nil, "", oauth2.NewError(oauth2.ErrorServerError)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Infof("Session %s token sent: clientID=%s", sessionID, creds.ID)
|
||||||
return jwt, refreshToken, nil
|
return jwt, refreshToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -487,7 +493,7 @@ func (s *Server) RefreshToken(creds oidc.ClientCredentials, token string) (*jose
|
||||||
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
return nil, oauth2.NewError(oauth2.ErrorServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Token refreshed sent: clientID=%s", creds.ID)
|
log.Infof("New token sent: clientID=%s", creds.ID)
|
||||||
|
|
||||||
return jwt, nil
|
return jwt, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -139,7 +140,7 @@ func TestServerNewSession(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := srv.NewSession("bogus_idpc", ci.Credentials.ID, state, ci.Metadata.RedirectURLs[0], nonce, false)
|
key, err := srv.NewSession("bogus_idpc", ci.Credentials.ID, state, ci.Metadata.RedirectURLs[0], nonce, false, []string{"openid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -195,7 +196,7 @@ func TestServerLogin(t *testing.T) {
|
||||||
|
|
||||||
sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo())
|
sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo())
|
||||||
sm.GenerateCode = staticGenerateCodeFunc("fakecode")
|
sm.GenerateCode = staticGenerateCodeFunc("fakecode")
|
||||||
sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURLs[0], "", false)
|
sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURLs[0], "", false, []string{"openid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -292,34 +293,52 @@ func TestServerCodeToken(t *testing.T) {
|
||||||
RefreshTokenRepo: refreshTokenRepo,
|
RefreshTokenRepo: refreshTokenRepo,
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false)
|
tests := []struct {
|
||||||
if err != nil {
|
scope []string
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
refreshToken string
|
||||||
}
|
}{
|
||||||
_, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{})
|
// No 'offline_access' in scope, should get empty refresh token.
|
||||||
if err != nil {
|
{
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
scope: []string{"openid"},
|
||||||
|
refreshToken: "",
|
||||||
|
},
|
||||||
|
// Have 'offline_access' in scope, should get non-empty refresh token.
|
||||||
|
{
|
||||||
|
scope: []string{"openid", "offline_access"},
|
||||||
|
refreshToken: "0/refresh-1",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = sm.AttachUser(sessionID, "testid-1")
|
for i, tt := range tests {
|
||||||
if err != nil {
|
sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false, tt.scope)
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
if err != nil {
|
||||||
}
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
|
}
|
||||||
|
_, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
key, err := sm.NewSessionKey(sessionID)
|
_, err = sm.AttachUser(sessionID, "testid-1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt, token, err := srv.CodeToken(ci.Credentials, key)
|
key, err := sm.NewSessionKey(sessionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
}
|
}
|
||||||
if jwt == nil {
|
|
||||||
t.Fatalf("Expected non-nil jwt")
|
jwt, token, err := srv.CodeToken(ci.Credentials, key)
|
||||||
}
|
if err != nil {
|
||||||
if token == "" {
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
t.Fatalf("Expected non-empty refresh token")
|
}
|
||||||
|
if jwt == nil {
|
||||||
|
t.Fatalf("case %d: expect non-nil jwt", i)
|
||||||
|
}
|
||||||
|
if token != tt.refreshToken {
|
||||||
|
t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,7 +362,7 @@ func TestServerTokenUnrecognizedKey(t *testing.T) {
|
||||||
ClientIdentityRepo: ciRepo,
|
ClientIdentityRepo: ciRepo,
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID, err := sm.NewSession("connector_id", ci.Credentials.ID, "bogus", url.URL{}, "", false)
|
sessionID, err := sm.NewSession("connector_id", ci.Credentials.ID, "bogus", url.URL{}, "", false, []string{"openid", "offline_access"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -375,16 +394,28 @@ func TestServerTokenFail(t *testing.T) {
|
||||||
signerFixture := &StaticSigner{sig: []byte("beer"), err: nil}
|
signerFixture := &StaticSigner{sig: []byte("beer"), err: nil}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
signer jose.Signer
|
signer jose.Signer
|
||||||
argCC oidc.ClientCredentials
|
argCC oidc.ClientCredentials
|
||||||
argKey string
|
argKey string
|
||||||
err string
|
err string
|
||||||
|
scope []string
|
||||||
|
refreshToken string
|
||||||
}{
|
}{
|
||||||
// control test case to make sure fixtures check out
|
// control test case to make sure fixtures check out
|
||||||
|
{
|
||||||
|
signer: signerFixture,
|
||||||
|
argCC: ccFixture,
|
||||||
|
argKey: keyFixture,
|
||||||
|
scope: []string{"openid", "offline_access"},
|
||||||
|
refreshToken: "0/refresh-1",
|
||||||
|
},
|
||||||
|
|
||||||
|
// no 'offline_access' in 'scope', should get empty refresh token
|
||||||
{
|
{
|
||||||
signer: signerFixture,
|
signer: signerFixture,
|
||||||
argCC: ccFixture,
|
argCC: ccFixture,
|
||||||
argKey: keyFixture,
|
argKey: keyFixture,
|
||||||
|
scope: []string{"openid"},
|
||||||
},
|
},
|
||||||
|
|
||||||
// unrecognized key
|
// unrecognized key
|
||||||
|
@ -393,6 +424,7 @@ func TestServerTokenFail(t *testing.T) {
|
||||||
argCC: ccFixture,
|
argCC: ccFixture,
|
||||||
argKey: "foo",
|
argKey: "foo",
|
||||||
err: oauth2.ErrorInvalidGrant,
|
err: oauth2.ErrorInvalidGrant,
|
||||||
|
scope: []string{"openid", "offline_access"},
|
||||||
},
|
},
|
||||||
|
|
||||||
// unrecognized client
|
// unrecognized client
|
||||||
|
@ -401,6 +433,7 @@ func TestServerTokenFail(t *testing.T) {
|
||||||
argCC: oidc.ClientCredentials{ID: "YYY"},
|
argCC: oidc.ClientCredentials{ID: "YYY"},
|
||||||
argKey: keyFixture,
|
argKey: keyFixture,
|
||||||
err: oauth2.ErrorInvalidClient,
|
err: oauth2.ErrorInvalidClient,
|
||||||
|
scope: []string{"openid", "offline_access"},
|
||||||
},
|
},
|
||||||
|
|
||||||
// signing operation fails
|
// signing operation fails
|
||||||
|
@ -409,6 +442,7 @@ func TestServerTokenFail(t *testing.T) {
|
||||||
argCC: ccFixture,
|
argCC: ccFixture,
|
||||||
argKey: keyFixture,
|
argKey: keyFixture,
|
||||||
err: oauth2.ErrorServerError,
|
err: oauth2.ErrorServerError,
|
||||||
|
scope: []string{"openid", "offline_access"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,7 +450,7 @@ func TestServerTokenFail(t *testing.T) {
|
||||||
sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo())
|
sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo())
|
||||||
sm.GenerateCode = func() (string, error) { return keyFixture, nil }
|
sm.GenerateCode = func() (string, error) { return keyFixture, nil }
|
||||||
|
|
||||||
sessionID, err := sm.NewSession("connector_id", ccFixture.ID, "bogus", url.URL{}, "", false)
|
sessionID, err := sm.NewSession("connector_id", ccFixture.ID, "bogus", url.URL{}, "", false, tt.scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -435,7 +469,7 @@ func TestServerTokenFail(t *testing.T) {
|
||||||
|
|
||||||
_, err = sm.AttachUser(sessionID, "testid-1")
|
_, err = sm.AttachUser(sessionID, "testid-1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("case %d: Unexpected error: %v", i, err)
|
t.Fatalf("case %d: unexpected error: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userRepo, err := makeNewUserRepo()
|
userRepo, err := makeNewUserRepo()
|
||||||
|
@ -463,22 +497,22 @@ func TestServerTokenFail(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt, token, err := srv.CodeToken(tt.argCC, tt.argKey)
|
jwt, token, err := srv.CodeToken(tt.argCC, tt.argKey)
|
||||||
|
if token != tt.refreshToken {
|
||||||
|
fmt.Printf("case %d: expect refresh token %q, got %q\n", i, tt.refreshToken, token)
|
||||||
|
t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token)
|
||||||
|
panic("")
|
||||||
|
}
|
||||||
if tt.err == "" {
|
if tt.err == "" {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("case %d: got non-nil error: %v", i, err)
|
t.Errorf("case %d: got non-nil error: %v", i, err)
|
||||||
} else if jwt == nil {
|
} else if jwt == nil {
|
||||||
t.Errorf("case %d: got nil JWT", i)
|
t.Errorf("case %d: got nil JWT", i)
|
||||||
} else if token == "" {
|
|
||||||
t.Errorf("case %d: got empty refresh token", i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if err.Error() != tt.err {
|
if err.Error() != tt.err {
|
||||||
t.Errorf("case %d: want err %q, got %q", i, tt.err, err.Error())
|
t.Errorf("case %d: want err %q, got %q", i, tt.err, err.Error())
|
||||||
} else if jwt != nil {
|
} else if jwt != nil {
|
||||||
t.Errorf("case %d: got non-nil JWT", i)
|
t.Errorf("case %d: got non-nil JWT", i)
|
||||||
} else if token != "" {
|
|
||||||
t.Errorf("case %d: got non-empty refresh token", i)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ type SessionManager struct {
|
||||||
keys SessionKeyRepo
|
keys SessionKeyRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SessionManager) NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool) (string, error) {
|
func (m *SessionManager) NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error) {
|
||||||
sID, err := m.GenerateCode()
|
sID, err := m.GenerateCode()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -62,6 +62,7 @@ func (m *SessionManager) NewSession(connectorID, clientID, clientState string, r
|
||||||
RedirectURL: redirectURL,
|
RedirectURL: redirectURL,
|
||||||
Register: register,
|
Register: register,
|
||||||
Nonce: nonce,
|
Nonce: nonce,
|
||||||
|
Scope: scope,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.sessions.Create(s)
|
err = m.sessions.Create(s)
|
||||||
|
|
|
@ -16,7 +16,7 @@ func staticGenerateCodeFunc(code string) GenerateCodeFunc {
|
||||||
func TestSessionManagerNewSession(t *testing.T) {
|
func TestSessionManagerNewSession(t *testing.T) {
|
||||||
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
||||||
sm.GenerateCode = staticGenerateCodeFunc("boo")
|
sm.GenerateCode = staticGenerateCodeFunc("boo")
|
||||||
got, err := sm.NewSession("bogus_idpc", "XXX", "bogus", url.URL{}, "", false)
|
got, err := sm.NewSession("bogus_idpc", "XXX", "bogus", url.URL{}, "", false, []string{"openid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func TestSessionManagerNewSession(t *testing.T) {
|
||||||
|
|
||||||
func TestSessionAttachRemoteIdentityTwice(t *testing.T) {
|
func TestSessionAttachRemoteIdentityTwice(t *testing.T) {
|
||||||
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
||||||
sessionID, err := sm.NewSession("bogus_idpc", "XXX", "bogus", url.URL{}, "", false)
|
sessionID, err := sm.NewSession("bogus_idpc", "XXX", "bogus", url.URL{}, "", false, []string{"openid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ func TestSessionAttachRemoteIdentityTwice(t *testing.T) {
|
||||||
|
|
||||||
func TestSessionManagerExchangeKey(t *testing.T) {
|
func TestSessionManagerExchangeKey(t *testing.T) {
|
||||||
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
||||||
sessionID, err := sm.NewSession("connector_id", "XXX", "bogus", url.URL{}, "", false)
|
sessionID, err := sm.NewSession("connector_id", "XXX", "bogus", url.URL{}, "", false, []string{"openid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ func TestSessionManagerGetSessionInStateNoExist(t *testing.T) {
|
||||||
|
|
||||||
func TestSessionManagerGetSessionInStateWrongState(t *testing.T) {
|
func TestSessionManagerGetSessionInStateWrongState(t *testing.T) {
|
||||||
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
||||||
sessionID, err := sm.NewSession("connector_id", "XXX", "bogus", url.URL{}, "", false)
|
sessionID, err := sm.NewSession("connector_id", "XXX", "bogus", url.URL{}, "", false, []string{"openid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ func TestSessionManagerGetSessionInStateWrongState(t *testing.T) {
|
||||||
|
|
||||||
func TestSessionManagerKill(t *testing.T) {
|
func TestSessionManagerKill(t *testing.T) {
|
||||||
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
sm := NewSessionManager(NewSessionRepo(), NewSessionKeyRepo())
|
||||||
sessionID, err := sm.NewSession("connector_id", "XXX", "bogus", url.URL{}, "", false)
|
sessionID, err := sm.NewSession("connector_id", "XXX", "bogus", url.URL{}, "", false, []string{"openid"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,9 @@ type Session struct {
|
||||||
|
|
||||||
// Nonce is optionally provided in the initial authorization request, and propogated in such cases to the generated claims.
|
// Nonce is optionally provided in the initial authorization request, and propogated in such cases to the generated claims.
|
||||||
Nonce string
|
Nonce string
|
||||||
|
|
||||||
|
// Scope is the 'scope' field in the authentication request. Example scopes are 'openid', 'email', 'offline', etc.
|
||||||
|
Scope []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claims returns a new set of Claims for the current session.
|
// Claims returns a new set of Claims for the current session.
|
||||||
|
|
Loading…
Reference in a new issue