diff --git a/.editorconfig b/.editorconfig index 27917441d8c11c43e8bf95bfc0e1da11d561d34f..593c02a66724d5ab44c2a131443df23375f98c70 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,7 @@ trim_trailing_whitespace = true [*.go] indent_style = tab -[*.proto] +[{*.proto,*.html}] indent_size = 2 [{Makefile,*.mk}] diff --git a/server/handlers.go b/server/handlers.go index a00b290b61d991cccb1a2f9e64d556d97ce0c435..ff17986a86bc85dee757c9594c1ed50b080e8262 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -632,13 +632,39 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: + if r.FormValue("alreadyApproved") == "true" { + s.sendCodeResponse(w, r, authReq) + return + } + client, err := s.storage.GetClient(ctx, authReq.ClientID) if err != nil { s.logger.ErrorContext(r.Context(), "Failed to get client", "client_id", authReq.ClientID, "err", err) s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve client.") return } - if err := s.templates.approval(r, w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes); err != nil { + + approvalSkip := (*approvalSkipData)(nil) + if sub, err := genSubject(authReq.Claims.UserID, authReq.ConnectorID); err == nil { + h := hmac.New(sha256.New, []byte(client.Secret)) + h.Write([]byte(client.ID)) + h.Write([]byte(sub)) + key := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + + h = hmac.New(sha256.New, []byte(client.Secret)) + h.Write([]byte(client.ID)) + scopes := make([]string, len(authReq.Scopes)) + copy(scopes, authReq.Scopes) + sort.Strings(scopes) + for _, scope := range scopes { + h.Write([]byte(scope)) + } + value := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) + + approvalSkip = &approvalSkipData{key, value} + } + + if err := s.templates.approval(r, w, authReq.ID, authReq.Claims.Username, client.Name, approvalSkip, authReq.Scopes); err != nil { s.logger.ErrorContext(r.Context(), "server template error", "err", err) } case http.MethodPost: diff --git a/server/templates.go b/server/templates.go index b77663e1f58d2569ea4acf1e7f4621922f350715..7f28b6b8db876e92cda718f5a2b0ae41a9ec0a0c 100644 --- a/server/templates.go +++ b/server/templates.go @@ -255,6 +255,11 @@ type connectorInfo struct { Type string } +type approvalSkipData struct { + Key string + Value string +} + type byName []connectorInfo func (n byName) Len() int { return len(n) } @@ -306,7 +311,7 @@ func (t *templates) password(r *http.Request, w http.ResponseWriter, postURL, la return renderTemplate(w, t.passwordTmpl, data) } -func (t *templates) approval(r *http.Request, w http.ResponseWriter, authReqID, username, clientName string, scopes []string) error { +func (t *templates) approval(r *http.Request, w http.ResponseWriter, authReqID, username, clientName string, approvalSkip *approvalSkipData, scopes []string) error { accesses := []string{} for _, scope := range scopes { access, ok := scopeDescriptions[scope] @@ -316,12 +321,13 @@ func (t *templates) approval(r *http.Request, w http.ResponseWriter, authReqID, } sort.Strings(accesses) data := struct { - User string - Client string - AuthReqID string - Scopes []string - ReqPath string - }{username, clientName, authReqID, accesses, r.URL.Path} + User string + ApprovalSkip *approvalSkipData + Client string + AuthReqID string + Scopes []string + ReqPath string + }{username, approvalSkip, clientName, authReqID, accesses, r.URL.Path} return renderTemplate(w, t.approvalTmpl, data) } diff --git a/web/templates/approval.html b/web/templates/approval.html index 1c037d2d2b9b53084d8ec6ec2566ae05d4d3082f..8e336d9bc72b96ab2eb603204702303f090d46f8 100644 --- a/web/templates/approval.html +++ b/web/templates/approval.html @@ -1,39 +1,81 @@ {{ template "header.html" . }} +{{- with .ApprovalSkip }} + {{- /* + WARNING: The following script should be the very first thing within this html snippet. + */ -}} + <script> + { + const approvalSkipKey = "approvalSkip.{{ .Key }}"; + const approvalSkipValue = "{{ .Value }}"; + const lastApprovalSkipValue = localStorage.getItem(approvalSkipKey); + + if (lastApprovalSkipValue && lastApprovalSkipValue === approvalSkipValue) { + const location = new URL(window.location.href); + location.searchParams.set("alreadyApproved", "true"); + window.location.replace(location.href); + } + + function removeScopes() { + localStorage.removeItem(approvalSkipKey); + } + + function saveScopes() { + if (document.getElementById("saveAccessGrant")?.checked) { + localStorage.setItem(approvalSkipKey, approvalSkipValue); + } else { + removeScopes() + } + } + + window.addEventListener("load", () => { + document.getElementById("save-access-grant-container").style.display = null; + }); + } + </script> +{{- end }} + <div class="theme-panel"> <h2 class="theme-heading">Grant Access</h2> <hr class="dex-separator"> <div> {{ if .Scopes }} - <div class="dex-subtle-text">{{ .Client }} would like to:</div> - <ul class="dex-list"> - {{ range $scope := .Scopes }} - <li>{{ $scope }}</li> - {{ end }} - </ul> + <div class="dex-subtle-text">{{ .Client }} would like to:</div> + <ul class="dex-list"> + {{ range $scope := .Scopes }} + <li>{{ $scope }}</li> + {{ end }} + </ul> {{ else }} - <div class="dex-subtle-text">{{ .Client }} has not requested any personal information</div> + <div class="dex-subtle-text">{{ .Client }} has not requested any personal information</div> {{ end }} </div> <hr class="dex-separator"> <div> + <div id="save-access-grant-container" style="display: none"> + <input type="checkbox" id="save-access-grant"> + <label for="save-access-grant"> + Save access grant for future logins + </label> + </div> + <div class="theme-form-row"> - <form method="post"> + <form method="post" onsubmit="saveScopes()"> <input type="hidden" name="req" value="{{ .AuthReqID }}"/> <input type="hidden" name="approval" value="approve"> <button type="submit" class="dex-btn theme-btn--success"> - <span class="dex-btn-text">Grant Access</span> + <span class="dex-btn-text">Grant Access</span> </button> </form> </div> <div class="theme-form-row"> - <form method="post"> + <form method="post" onsubmit="removeScopes()"> <input type="hidden" name="req" value="{{ .AuthReqID }}"/> <input type="hidden" name="approval" value="rejected"> <button type="submit" class="dex-btn theme-btn-provider"> - <span class="dex-btn-text">Cancel</span> + <span class="dex-btn-text">Cancel</span> </button> </form> </div>