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>