|
|
|
@@ -179,7 +179,13 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|
|
|
|
|
authReqID := r.FormValue("req")
|
|
|
|
|
|
|
|
|
|
// TODO(ericchiang): cache user identity.
|
|
|
|
|
authReq, err := s.storage.GetAuthRequest(authReqID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Failed to get auth request: %v", err)
|
|
|
|
|
s.renderError(w, http.StatusInternalServerError, errServerError, "")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
scopes := parseScopes(authReq.Scopes)
|
|
|
|
|
|
|
|
|
|
switch r.Method {
|
|
|
|
|
case "GET":
|
|
|
|
@@ -199,7 +205,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// Use the auth request ID as the "state" token.
|
|
|
|
|
//
|
|
|
|
|
// TODO(ericchiang): Is this appropriate or should we also be using a nonce?
|
|
|
|
|
callbackURL, err := conn.LoginURL(s.absURL("/callback"), authReqID)
|
|
|
|
|
callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReqID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Connector %q returned error when creating callback: %v", connID, err)
|
|
|
|
|
s.renderError(w, http.StatusInternalServerError, errServerError, "")
|
|
|
|
@@ -221,7 +227,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
username := r.FormValue("login")
|
|
|
|
|
password := r.FormValue("password")
|
|
|
|
|
|
|
|
|
|
identity, ok, err := passwordConnector.Login(username, password)
|
|
|
|
|
identity, ok, err := passwordConnector.Login(r.Context(), scopes, username, password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Failed to login user: %v", err)
|
|
|
|
|
s.renderError(w, http.StatusInternalServerError, errServerError, "")
|
|
|
|
@@ -231,12 +237,6 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
s.templates.password(w, authReqID, r.URL.String(), username, true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
authReq, err := s.storage.GetAuthRequest(authReqID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Failed to get auth request: %v", err)
|
|
|
|
|
s.renderError(w, http.StatusInternalServerError, errServerError, "")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
redirectURL, err := s.finalizeLogin(identity, authReq, conn.Connector)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Failed to finalize login: %v", err)
|
|
|
|
@@ -286,7 +286,7 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
identity, err := callbackConnector.HandleCallback(r)
|
|
|
|
|
identity, err := callbackConnector.HandleCallback(parseScopes(authReq.Scopes), r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Failed to authenticate: %v", err)
|
|
|
|
|
s.renderError(w, http.StatusInternalServerError, errServerError, "")
|
|
|
|
@@ -304,34 +304,12 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.AuthRequest, conn connector.Connector) (string, error) {
|
|
|
|
|
if authReq.ConnectorID == "" {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
claims := storage.Claims{
|
|
|
|
|
UserID: identity.UserID,
|
|
|
|
|
Username: identity.Username,
|
|
|
|
|
Email: identity.Email,
|
|
|
|
|
EmailVerified: identity.EmailVerified,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
groupsConn, ok := conn.(connector.GroupsConnector)
|
|
|
|
|
if ok {
|
|
|
|
|
reqGroups := func() bool {
|
|
|
|
|
for _, scope := range authReq.Scopes {
|
|
|
|
|
if scope == scopeGroups {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}()
|
|
|
|
|
if reqGroups {
|
|
|
|
|
groups, err := groupsConn.Groups(identity)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("getting groups: %v", err)
|
|
|
|
|
}
|
|
|
|
|
claims.Groups = groups
|
|
|
|
|
}
|
|
|
|
|
Groups: identity.Groups,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
|
|
|
|
@@ -407,14 +385,15 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
|
|
|
|
|
switch responseType {
|
|
|
|
|
case responseTypeCode:
|
|
|
|
|
code := storage.AuthCode{
|
|
|
|
|
ID: storage.NewID(),
|
|
|
|
|
ClientID: authReq.ClientID,
|
|
|
|
|
ConnectorID: authReq.ConnectorID,
|
|
|
|
|
Nonce: authReq.Nonce,
|
|
|
|
|
Scopes: authReq.Scopes,
|
|
|
|
|
Claims: authReq.Claims,
|
|
|
|
|
Expiry: s.now().Add(time.Minute * 30),
|
|
|
|
|
RedirectURI: authReq.RedirectURI,
|
|
|
|
|
ID: storage.NewID(),
|
|
|
|
|
ClientID: authReq.ClientID,
|
|
|
|
|
ConnectorID: authReq.ConnectorID,
|
|
|
|
|
Nonce: authReq.Nonce,
|
|
|
|
|
Scopes: authReq.Scopes,
|
|
|
|
|
Claims: authReq.Claims,
|
|
|
|
|
Expiry: s.now().Add(time.Minute * 30),
|
|
|
|
|
RedirectURI: authReq.RedirectURI,
|
|
|
|
|
ConnectorData: authReq.ConnectorData,
|
|
|
|
|
}
|
|
|
|
|
if err := s.storage.CreateAuthCode(code); err != nil {
|
|
|
|
|
log.Printf("Failed to create auth code: %v", err)
|
|
|
|
@@ -537,12 +516,13 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
|
|
|
|
|
var refreshToken string
|
|
|
|
|
if reqRefresh {
|
|
|
|
|
refresh := storage.RefreshToken{
|
|
|
|
|
RefreshToken: storage.NewID(),
|
|
|
|
|
ClientID: authCode.ClientID,
|
|
|
|
|
ConnectorID: authCode.ConnectorID,
|
|
|
|
|
Scopes: authCode.Scopes,
|
|
|
|
|
Claims: authCode.Claims,
|
|
|
|
|
Nonce: authCode.Nonce,
|
|
|
|
|
RefreshToken: storage.NewID(),
|
|
|
|
|
ClientID: authCode.ClientID,
|
|
|
|
|
ConnectorID: authCode.ConnectorID,
|
|
|
|
|
Scopes: authCode.Scopes,
|
|
|
|
|
Claims: authCode.Claims,
|
|
|
|
|
Nonce: authCode.Nonce,
|
|
|
|
|
ConnectorData: authCode.ConnectorData,
|
|
|
|
|
}
|
|
|
|
|
if err := s.storage.CreateRefresh(refresh); err != nil {
|
|
|
|
|
log.Printf("failed to create refresh token: %v", err)
|
|
|
|
@@ -574,6 +554,10 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Per the OAuth2 spec, if the client has omitted the scopes, default to the original
|
|
|
|
|
// authorized scopes.
|
|
|
|
|
//
|
|
|
|
|
// https://tools.ietf.org/html/rfc6749#section-6
|
|
|
|
|
scopes := refresh.Scopes
|
|
|
|
|
if scope != "" {
|
|
|
|
|
requestedScopes := strings.Fields(scope)
|
|
|
|
@@ -601,7 +585,43 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
|
|
|
|
|
scopes = requestedScopes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO(ericchiang): re-auth with backends
|
|
|
|
|
conn, ok := s.connectors[refresh.ConnectorID]
|
|
|
|
|
if !ok {
|
|
|
|
|
log.Printf("connector ID not found: %q", refresh.ConnectorID)
|
|
|
|
|
tokenErr(w, errServerError, "", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Can the connector refresh the identity? If so, attempt to refresh the data
|
|
|
|
|
// in the connector.
|
|
|
|
|
//
|
|
|
|
|
// TODO(ericchiang): We may want a strict mode where connectors that don't implement
|
|
|
|
|
// this interface can't perform refreshing.
|
|
|
|
|
if refreshConn, ok := conn.Connector.(connector.RefreshConnector); ok {
|
|
|
|
|
ident := connector.Identity{
|
|
|
|
|
UserID: refresh.Claims.UserID,
|
|
|
|
|
Username: refresh.Claims.Username,
|
|
|
|
|
Email: refresh.Claims.Email,
|
|
|
|
|
EmailVerified: refresh.Claims.EmailVerified,
|
|
|
|
|
Groups: refresh.Claims.Groups,
|
|
|
|
|
ConnectorData: refresh.ConnectorData,
|
|
|
|
|
}
|
|
|
|
|
ident, err := refreshConn.Refresh(r.Context(), parseScopes(scopes), ident)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("failed to refresh identity: %v", err)
|
|
|
|
|
tokenErr(w, errServerError, "", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the claims of the refresh token.
|
|
|
|
|
//
|
|
|
|
|
// UserID intentionally ignored for now.
|
|
|
|
|
refresh.Claims.Username = ident.Username
|
|
|
|
|
refresh.Claims.Email = ident.Email
|
|
|
|
|
refresh.Claims.EmailVerified = ident.EmailVerified
|
|
|
|
|
refresh.Claims.Groups = ident.Groups
|
|
|
|
|
refresh.ConnectorData = ident.ConnectorData
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
idToken, expiry, err := s.newIDToken(client.ID, refresh.Claims, scopes, refresh.Nonce)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -610,6 +630,8 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh tokens are claimed exactly once. Delete the current token and
|
|
|
|
|
// create a new one.
|
|
|
|
|
if err := s.storage.DeleteRefresh(code); err != nil {
|
|
|
|
|
log.Printf("failed to delete auth code: %v", err)
|
|
|
|
|
tokenErr(w, errServerError, "", http.StatusInternalServerError)
|
|
|
|
|