changelog-webapp: Added signout
This CL adds the ability to signout and changes the login flow. Users will only receive a login prompt as a first-time/signed out user, or after a month of inactivity. These users will be prompted to select an a account and grant permissions to the app. Otherwise, if the token is only expired, the login flow is completed automatically to receive a new access token.
BUG=b/160901711
TEST=run local
Change-Id: Ic296ebab1b9a6bc60b8ce3593f9aa56c8168fbbc
diff --git a/src/cmd/changelog-webapp/controllers/authHandlers.go b/src/cmd/changelog-webapp/controllers/authHandlers.go
index aa159ee..34148bc 100644
--- a/src/cmd/changelog-webapp/controllers/authHandlers.go
+++ b/src/cmd/changelog-webapp/controllers/authHandlers.go
@@ -16,7 +16,6 @@
import (
"context"
- "errors"
"fmt"
"math/rand"
"net/http"
@@ -35,7 +34,6 @@
// Session variables
sessionName = "changelog"
sessionKeyLength = 32
- sessionAge = 84600
// Oauth state generation variables
oauthStateCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"
@@ -54,10 +52,6 @@
var clientSecretName = os.Getenv("COS_CHANGELOG_CLIENT_SECRET_NAME")
var sessionSecretName = os.Getenv("COS_CHANGELOG_SESSION_SECRET_NAME")
-// ErrorSessionRetrieval indicates that a request has no session, or the
-// session was malformed.
-var ErrorSessionRetrieval = errors.New("No session found")
-
func init() {
var err error
client, err := secretmanager.NewClient(context.Background())
@@ -74,7 +68,6 @@
log.Fatalf("Failed to retrieve secret :%s\n%v", sessionSecretName, err)
}
store = sessions.NewCookieStore([]byte(sessionSecret))
- store.MaxAge(sessionAge)
}
// Retrieve secrets stored in Gcloud Secret Manager
@@ -102,27 +95,54 @@
return state[oauthStateLength:]
}
-// HTTPClient creates an authorized HTTP Client using stored token credentials.
-// Returns error if no session or a malformed session is detected.
-// Otherwise returns an HTTP Client with the stored Oauth access token.
-// If the access token is expired, automatically refresh the token
-func HTTPClient(w http.ResponseWriter, r *http.Request, returnURL string) (*http.Client, error) {
+// tokenExpired indicates whether the Oauth token associated with a request is expired
+func tokenExpired(r *http.Request) bool {
var parsedExpiry time.Time
+ session, _ := store.Get(r, sessionName)
+ parsedExpiry, err := time.Parse(time.RFC3339, session.Values["expiry"].(string))
+ return err != nil || parsedExpiry.Before(time.Now())
+}
+
+// SignedIn returns a bool indicating if the current request is signed in
+func SignedIn(r *http.Request) bool {
session, err := store.Get(r, sessionName)
if err != nil || session.IsNew {
- return nil, ErrorSessionRetrieval
+ return false
}
for _, key := range []string{"accessToken", "refreshToken", "tokenType", "expiry"} {
if val, ok := session.Values[key]; !ok || val == nil {
- return nil, ErrorSessionRetrieval
+ return false
}
}
- if parsedExpiry, err = time.Parse(time.RFC3339, session.Values["expiry"].(string)); err != nil {
- return nil, ErrorSessionRetrieval
+ return true
+}
+
+// RequireToken will check if the user has a valid, unexpired Oauth token.
+// If not, it will initiate the Oauth flow.
+// Returns a bool indicating if the user was redirected
+func RequireToken(w http.ResponseWriter, r *http.Request, activePage string) bool {
+ if !SignedIn(r) {
+ err := promptLoginTemplate.Execute(w, &statusPage{ActivePage: activePage})
+ if err != nil {
+ log.Errorf("RequireToken: error executing promptLogin template: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ return true
}
- if parsedExpiry.Before(time.Now()) {
- log.Debug("HTTPClient: Token expired, calling Oauth flow")
- HandleLogin(w, r, returnURL)
+ // If token is expired, auto refresh instead of prompting sign in
+ if tokenExpired(r) {
+ HandleLogin(w, r, activePage, false)
+ return true
+ }
+ return false
+}
+
+// HTTPClient creates an authorized HTTP Client using stored token credentials.
+func HTTPClient(w http.ResponseWriter, r *http.Request) (*http.Client, error) {
+ session, _ := store.Get(r, sessionName)
+ parsedExpiry, err := time.Parse(time.RFC3339, session.Values["expiry"].(string))
+ if err != nil {
+ return nil, err
}
token := &oauth2.Token{
AccessToken: session.Values["accessToken"].(string),
@@ -134,9 +154,9 @@
}
// HandleLogin initiates the Oauth flow.
-func HandleLogin(w http.ResponseWriter, r *http.Request, returnURL string) {
+func HandleLogin(w http.ResponseWriter, r *http.Request, returnURL string, explicitApproval bool) {
state := randomString(oauthStateLength, returnURL)
- // Ignore store.Get() errors in HandleLogin because an error indicates the
+ // Ignore store.Get() errors because an error indicates the
// old session could not be deciphered. It returns a new session
// regardless.
session, _ := store.Get(r, sessionName)
@@ -147,8 +167,12 @@
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
-
- authURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline)
+ var authURL string
+ if explicitApproval {
+ authURL = config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
+ } else {
+ authURL = config.AuthCodeURL(state, oauth2.AccessTypeOffline)
+ }
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
}
@@ -197,6 +221,30 @@
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
+ http.Redirect(w, r, returnURLFromState(sessionState), http.StatusTemporaryRedirect)
+}
- http.Redirect(w, r, returnURLFromState(sessionState), http.StatusPermanentRedirect)
+// HandleSignOut signs out the user by removing token information from the
+// session
+func HandleSignOut(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ log.Errorf("Could not parse request: %v\n", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ redirect := r.FormValue("redirect")
+ if redirect == "" {
+ redirect = "/"
+ }
+ session, _ := store.Get(r, sessionName)
+ session.Values["accessToken"] = nil
+ session.Values["refreshToken"] = nil
+ session.Values["tokenType"] = nil
+ session.Values["expiry"] = nil
+ err := session.Save(r, w)
+ if err != nil {
+ log.Errorf("HandleCallback: Error saving session: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ http.Redirect(w, r, redirect, http.StatusTemporaryRedirect)
}
diff --git a/src/cmd/changelog-webapp/controllers/pageHandlers.go b/src/cmd/changelog-webapp/controllers/pageHandlers.go
index 909c057..911dd46 100644
--- a/src/cmd/changelog-webapp/controllers/pageHandlers.go
+++ b/src/cmd/changelog-webapp/controllers/pageHandlers.go
@@ -119,12 +119,14 @@
type statusPage struct {
ActivePage string
+ SignedIn bool
}
type basicTextPage struct {
Header string
Body string
ActivePage string
+ SignedIn bool
}
type repoTable struct {
@@ -268,34 +270,39 @@
}
// handleError creates the error page for a given error
-func handleError(w http.ResponseWriter, err utils.ChangelogError, currPage string) {
- basicTextTemplate.Execute(w, &basicTextPage{
- Header: err.HTTPStatus(),
- Body: err.Error(),
+func handleError(w http.ResponseWriter, r *http.Request, displayErr utils.ChangelogError, currPage string) {
+ err := basicTextTemplate.Execute(w, &basicTextPage{
+ Header: displayErr.HTTPStatus(),
+ Body: displayErr.Error(),
ActivePage: currPage,
+ SignedIn: SignedIn(r),
})
+ if err != nil {
+ log.Error(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
// HandleIndex serves the home page
func HandleIndex(w http.ResponseWriter, r *http.Request) {
- indexTemplate.Execute(w, nil)
+ err := indexTemplate.Execute(w, &statusPage{SignedIn: SignedIn(r)})
+ if err != nil {
+ log.Error(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
}
// HandleChangelog serves the changelog page
func HandleChangelog(w http.ResponseWriter, r *http.Request) {
- httpClient, err := HTTPClient(w, r, "/changelog/")
- if err != nil {
- log.Debug(err)
- err = promptLoginTemplate.Execute(w, &statusPage{ActivePage: "/changelog/"})
- if err != nil {
- log.Errorf("HandleChangelog: error executing promptLogin template: %v", err)
- }
+ if RequireToken(w, r, "/changelog/") {
return
}
+ var err error
if err := r.ParseForm(); err != nil {
err = changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize})
if err != nil {
log.Errorf("HandleChangelog: error executing locatebuild template: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
@@ -306,6 +313,7 @@
err = changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize, Internal: true})
if err != nil {
log.Errorf("HandleChangelog: error executing locatebuild template: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
@@ -317,11 +325,16 @@
if r.FormValue("internal") == "true" {
internal, instance, manifestRepo = true, internalGoBInstance, internalManifestRepo
}
+ httpClient, err := HTTPClient(w, r)
+ if err != nil {
+ HandleLogin(w, r, "/changelog/", false)
+ return
+ }
added, removed, utilErr := changelog.Changelog(httpClient, source, target, instance, manifestRepo, querySize)
if utilErr != nil {
log.Errorf("HandleChangelog: error retrieving changelog between builds %s and %s on GoB instance: %s with manifest repository: %s\n%v\n",
source, target, externalGoBInstance, externalManifestRepo, utilErr)
- handleError(w, utilErr, "/changelog/")
+ handleError(w, r, utilErr, "/changelog/")
return
}
page := createChangelogPage(changelogData{
@@ -335,25 +348,21 @@
err = changelogTemplate.Execute(w, page)
if err != nil {
log.Errorf("HandleChangelog: error executing changelog template: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// HandleLocateBuild serves the Locate CL page
func HandleLocateBuild(w http.ResponseWriter, r *http.Request) {
- httpClient, err := HTTPClient(w, r, "/locatebuild/")
- // Require login to access if no session found
- if err != nil {
- log.Debug(err)
- err = promptLoginTemplate.Execute(w, &statusPage{ActivePage: "/locatebuild/"})
- if err != nil {
- log.Errorf("HandleLocateBuild: error executing promptLogin template: %v", err)
- }
+ if RequireToken(w, r, "/locatebuild/") {
return
}
- if err := r.ParseForm(); err != nil {
+ var err error
+ if err = r.ParseForm(); err != nil {
err = locateBuildTemplate.Execute(w, &locateBuildPage{Internal: true})
if err != nil {
log.Errorf("HandleLocateBuild: error executing locatebuild template: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
@@ -363,6 +372,7 @@
err = locateBuildTemplate.Execute(w, &locateBuildPage{Internal: true})
if err != nil {
log.Errorf("HandleLocateBuild: error executing locatebuild template: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
@@ -370,10 +380,15 @@
if r.FormValue("internal") == "true" {
internal, gerrit, fallbackGerrit, gob, repo = true, internalGerritInstance, internalFallbackGerritInstance, internalGoBInstance, internalManifestRepo
}
+ httpClient, err := HTTPClient(w, r)
+ if err != nil {
+ HandleLogin(w, r, "/locatebuild/", false)
+ return
+ }
buildData, didFallback, utilErr := findBuildWithFallback(httpClient, gerrit, fallbackGerrit, gob, repo, cl, internal)
if utilErr != nil {
log.Errorf("HandleLocateBuild: error retrieving build for CL %s with internal set to %t\n%v", cl, internal, utilErr)
- handleError(w, utilErr, "/locatebuild/")
+ handleError(w, r, utilErr, "/locatebuild/")
return
}
var gerritLink string
@@ -392,5 +407,6 @@
err = locateBuildTemplate.Execute(w, page)
if err != nil {
log.Errorf("HandleLocateBuild: error executing locatebuild template: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
diff --git a/src/cmd/changelog-webapp/main.go b/src/cmd/changelog-webapp/main.go
index 3813591..2fef7e0 100644
--- a/src/cmd/changelog-webapp/main.go
+++ b/src/cmd/changelog-webapp/main.go
@@ -41,9 +41,10 @@
http.HandleFunc("/changelog/", controllers.HandleChangelog)
http.HandleFunc("/locatebuild/", controllers.HandleLocateBuild)
http.HandleFunc("/login/", func(w http.ResponseWriter, r *http.Request) {
- controllers.HandleLogin(w, r, "/")
+ controllers.HandleLogin(w, r, "/", true)
})
http.HandleFunc("/oauth2callback/", controllers.HandleCallback)
+ http.HandleFunc("/signout/", controllers.HandleSignOut)
if port == "" {
port = "8081"
diff --git a/src/cmd/changelog-webapp/static/css/base.css b/src/cmd/changelog-webapp/static/css/base.css
index 7d763c7..19411eb 100644
--- a/src/cmd/changelog-webapp/static/css/base.css
+++ b/src/cmd/changelog-webapp/static/css/base.css
@@ -44,7 +44,6 @@
background-color: #4285f4;
display: flex;
height: 50px;
- padding: 0px 25px;
width: 100%;
}
@@ -52,6 +51,17 @@
color: #f8f8f8;
font-size: 24;
font-weight: 350;
+ padding-left: 25px;
+}
+
+a.login,
+a.signout {
+ color: #f8f8f8;
+ float: right;
+ font-size: 16;
+ font-weight: 16px;
+ margin-left: auto;
+ padding-right: 25px;
}
.sidenav {
diff --git a/src/cmd/changelog-webapp/static/templates/changelog.html b/src/cmd/changelog-webapp/static/templates/changelog.html
index a45b767..2a69afb 100644
--- a/src/cmd/changelog-webapp/static/templates/changelog.html
+++ b/src/cmd/changelog-webapp/static/templates/changelog.html
@@ -7,12 +7,12 @@
<body>
<div class="navbar">
<p class="navbar-title">Container Optimized OS</p>
+ <a class="signout" href="/signout/?redirect=/changelog/">Sign Out</a>
</div>
<div class="sidenav">
<a href="/">Home</a>
<a class="active" href="/changelog/">Changelog</a>
<a href="/locatebuild/">Locate Build</a>
- <a href="/login/">Login</a>
</div>
<div class="main">
<h1>Search Changelog</h1>
diff --git a/src/cmd/changelog-webapp/static/templates/error.html b/src/cmd/changelog-webapp/static/templates/error.html
index 21f8a40..68433e7 100644
--- a/src/cmd/changelog-webapp/static/templates/error.html
+++ b/src/cmd/changelog-webapp/static/templates/error.html
@@ -6,6 +6,11 @@
<body>
<div class="navbar">
<p class="navbar-title">Container Optimized OS</p>
+ {{if .SignedIn}}
+ <a class="signout" href="/signout/">Sign Out</a>
+ {{else}}
+ <a class="login" href="/login/">Login</a>
+ {{end}}
</div>
<div class="sidenav">
{{if (eq .ActivePage "home")}}
@@ -23,7 +28,6 @@
{{else}}
<a href="/locatebuild/">Locate Build</a>
{{end}}
- <a href="/login/">Login</a>
</div>
<div class="main">
<div class="text-content">
diff --git a/src/cmd/changelog-webapp/static/templates/index.html b/src/cmd/changelog-webapp/static/templates/index.html
index 350a422..a256eeb 100644
--- a/src/cmd/changelog-webapp/static/templates/index.html
+++ b/src/cmd/changelog-webapp/static/templates/index.html
@@ -6,12 +6,16 @@
<body>
<div class="navbar">
<p class="navbar-title">Container Optimized OS</p>
+ {{if .SignedIn}}
+ <a class="signout" href="/signout/">Sign Out</a>
+ {{else}}
+ <a class="login" href="/login/">Login</a>
+ {{end}}
</div>
<div class="sidenav">
<a class="active" href="/">Home</a>
<a href="/changelog/">Changelog</a>
<a href="/locatebuild/">Locate Build</a>
- <a href="/login/">Login</a>
</div>
<div class="main">
<div class="text-content">
diff --git a/src/cmd/changelog-webapp/static/templates/locateBuild.html b/src/cmd/changelog-webapp/static/templates/locateBuild.html
index 8cf8fbb..fe57c76 100644
--- a/src/cmd/changelog-webapp/static/templates/locateBuild.html
+++ b/src/cmd/changelog-webapp/static/templates/locateBuild.html
@@ -7,12 +7,12 @@
<body>
<div class="navbar">
<p class="navbar-title">Container Optimized OS</p>
+ <a class="signout" href="/signout/?redirect=/locatebuild/">Sign Out</a>
</div>
<div class="sidenav">
<a href="/">Home</a>
<a href="/changelog/">Changelog</a>
<a class="active" href="/locatebuild/">Locate Build</a>
- <a href="/login/">Login</a>
</div>
<div class="main">
<h1>Locate Build with CL</h1>
diff --git a/src/cmd/changelog-webapp/static/templates/promptLogin.html b/src/cmd/changelog-webapp/static/templates/promptLogin.html
index 9ca8fb9..425cf87 100644
--- a/src/cmd/changelog-webapp/static/templates/promptLogin.html
+++ b/src/cmd/changelog-webapp/static/templates/promptLogin.html
@@ -6,6 +6,7 @@
<body>
<div class="navbar">
<p class="navbar-title">Container Optimized OS</p>
+ <a class="login" href="/login/">Login</a>
</div>
<div class="sidenav">
<a href="/">Home</a>
@@ -19,7 +20,6 @@
{{else}}
<a href="/locatebuild/">Locate Build</a>
{{end}}
- <a href="/login/">Login</a>
</div>
<div class="main">
<div class="text-content">