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

Fix for https://www.pivotaltracker.com/story/show/163470469 #2237

Merged
merged 7 commits into from Jun 17, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 45 additions & 2 deletions pkg/auth/authentication/auth.go
@@ -1,6 +1,8 @@
package authentication

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -133,11 +135,19 @@ func (h LogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}

// LoginStateCookieName is the name given to the cookie storing the encrypted Login.gov state nonce.
const LoginStateCookieName = "LGState"

// RedirectHandler handles redirection
type RedirectHandler struct {
Context
}

func shaAsString(nonce string) string {
s := sha256.Sum256([]byte(nonce))
return hex.EncodeToString(s[:])
}

// RedirectHandler constructs the Login.gov authentication URL and redirects to it
func (h RedirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session := auth.SessionFromRequestContext(r)
Expand All @@ -147,13 +157,26 @@ func (h RedirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

authURL, err := h.loginGovProvider.AuthorizationURL(r)
loginData, err := h.loginGovProvider.AuthorizationURL(r)
if err != nil {
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}

http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
// Hash the state/Nonce value sent to login.gov and set the result as an HttpOnly cookie
// Check this when we return from login.gov
stateCookie := http.Cookie{
Name: LoginStateCookieName,
ntwyman marked this conversation as resolved.
Show resolved Hide resolved
Value: shaAsString(loginData.Nonce),
Path: "/",
Expires: time.Unix(0, 0),
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: true,
}
http.SetCookie(w, &stateCookie)
http.Redirect(w, r, loginData.RedirectURL, http.StatusTemporaryRedirect)
}

// CallbackHandler processes a callback from login.gov
Expand Down Expand Up @@ -189,6 +212,7 @@ func (h CallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}

rawLandingURL := h.landingURL(session)
landingURL, err := url.Parse(rawLandingURL)
if err != nil {
Expand All @@ -214,6 +238,25 @@ func (h CallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

// Check the state value sent back from login.gov with the value saved in the cookie
returnedState := r.URL.Query().Get("state")
stateCookie, err := r.Cookie(LoginStateCookieName)
if err != nil {
h.logger.Error("Getting login.gov state cookie", zap.Error(err))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}

hash := stateCookie.Value
if hash != shaAsString(returnedState) {
h.logger.Error("Returned state does not match cookie",
ntwyman marked this conversation as resolved.
Show resolved Hide resolved
zap.String("state", returnedState),
zap.String("cookie", hash),
zap.String("hash", shaAsString(returnedState)))
http.Error(w, http.StatusText(500), http.StatusInternalServerError)
return
}

provider, err := getLoginGovProviderForRequest(r)
if err != nil {
h.logger.Error("Get Goth provider", zap.Error(err))
Expand Down
19 changes: 13 additions & 6 deletions pkg/auth/authentication/login_gov.go
Expand Up @@ -105,30 +105,37 @@ func generateNonce() string {
return base64.URLEncoding.EncodeToString(nonceBytes)
}

// LoginGovData contains the URL and State nonce used to redirect a user
// login.gov for authentication
type LoginGovData struct {
RedirectURL string
Nonce string
ntwyman marked this conversation as resolved.
Show resolved Hide resolved
}

// AuthorizationURL returns a URL for login.gov authorization with required params
func (p LoginGovProvider) AuthorizationURL(r *http.Request) (string, error) {
func (p LoginGovProvider) AuthorizationURL(r *http.Request) (*LoginGovData, error) {
provider, err := getLoginGovProviderForRequest(r)
if err != nil {
p.logger.Error("Get Goth provider", zap.Error(err))
return "", err
return nil, err
}
state := generateNonce()
sess, err := provider.BeginAuth(state)
if err != nil {
p.logger.Error("Goth begin auth", zap.Error(err))
return "", err
return nil, err
}

baseURL, err := sess.GetAuthURL()
if err != nil {
p.logger.Error("Goth get auth URL", zap.Error(err))
return "", err
return nil, err
}

authURL, err := url.Parse(baseURL)
if err != nil {
p.logger.Error("Parse auth URL", zap.Error(err))
return "", err
return nil, err
}

params := authURL.Query()
Expand All @@ -137,7 +144,7 @@ func (p LoginGovProvider) AuthorizationURL(r *http.Request) (string, error) {
params.Set("scope", "openid email")

authURL.RawQuery = params.Encode()
return authURL.String(), nil
return &LoginGovData{authURL.String(), state}, nil
}

// LogoutURL returns a full URL to log out of login.gov with required params
Expand Down