From a324b6c2d3712dddb27d47d12c4e9e985300aaef Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Sun, 18 Feb 2024 22:26:44 +0100 Subject: [PATCH] Split HTTP handlers in separate files --- client.go | 38 ++++++++++++++++++++++++++++++++++++++ main.go | 405 +---------------------------------------------------- oauth2.go | 294 +++++++++++++++++++++++++++++++++++++++++++++++++++++ user.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/client.go b/client.go new file mode 100644 index 0000000000000000000000000000000000000000..d26317299c1c649a42e433dc123aa2751d8b6a20 --- /dev/null +++ b/client.go @@ -0,0 +1,38 @@ +package main + +import ( + "net/http" +) + +func createClient(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + db := dbFromContext(ctx) + tpl := templateFromContext(ctx) + + loginToken := loginTokenFromContext(ctx) + if loginToken == nil { + http.Redirect(w, req, "/login", http.StatusFound) + return + } + + client, clientSecret, err := NewClient(loginToken.User) + if err != nil { + httpError(w, err) + return + } + if err := db.StoreClient(ctx, client); err != nil { + httpError(w, err) + return + } + + data := struct { + ClientID string + ClientSecret string + }{ + ClientID: client.ClientID, + ClientSecret: clientSecret, + } + if err := tpl.ExecuteTemplate(w, "client-secret.html", &data); err != nil { + panic(err) + } +} diff --git a/main.go b/main.go index 195d546f75cabd56a04c9d00e2ddedab7c111472..e6d5e7e9b49df2756caa03580139b79c85909961 100644 --- a/main.go +++ b/main.go @@ -3,21 +3,12 @@ import ( "context" "embed" - "encoding/json" - "errors" "flag" - "fmt" "html/template" - "io" "log" - "mime" "net" "net/http" - "net/url" - "strings" - "time" - "git.sr.ht/~emersion/go-oauth2" "github.com/go-chi/chi/v5" ) @@ -41,318 +32,12 @@ log.Fatalf("Failed to open DB: %v", err) } mux := chi.NewRouter() - mux.Handle("/static/*", http.FileServer(http.FS(staticFS))) - - mux.Get("/", func(w http.ResponseWriter, req *http.Request) { - ctx := req.Context() - - loginToken := loginTokenFromContext(ctx) - if loginToken == nil { - http.Redirect(w, req, "/login", http.StatusFound) - return - } - - clients, err := db.ListClients(ctx, loginToken.User) - if err != nil { - httpError(w, err) - return - } - - if err := tpl.ExecuteTemplate(w, "index.html", clients); err != nil { - panic(err) - } - }) - - mux.Post("/client/new", func(w http.ResponseWriter, req *http.Request) { - ctx := req.Context() - - loginToken := loginTokenFromContext(ctx) - if loginToken == nil { - http.Redirect(w, req, "/login", http.StatusFound) - return - } - - client, clientSecret, err := NewClient(loginToken.User) - if err != nil { - httpError(w, err) - return - } - if err := db.StoreClient(ctx, client); err != nil { - httpError(w, err) - return - } - - data := struct { - ClientID string - ClientSecret string - }{ - ClientID: client.ClientID, - ClientSecret: clientSecret, - } - if err := tpl.ExecuteTemplate(w, "client-secret.html", &data); err != nil { - panic(err) - } - }) - - mux.HandleFunc("/login", func(w http.ResponseWriter, req *http.Request) { - ctx := req.Context() - - q := req.URL.Query() - rawRedirectURI := q.Get("redirect_uri") - if rawRedirectURI == "" { - rawRedirectURI = "/" - } - - redirectURI, err := url.Parse(rawRedirectURI) - if err != nil || redirectURI.Scheme != "" || redirectURI.Opaque != "" || redirectURI.User != nil || redirectURI.Host != "" { - http.Error(w, "Invalid redirect URI", http.StatusBadRequest) - return - } - - if loginTokenFromContext(ctx) != nil { - http.Redirect(w, req, redirectURI.String(), http.StatusFound) - return - } - - username := req.PostFormValue("username") - password := req.PostFormValue("password") - if username == "" { - if err := tpl.ExecuteTemplate(w, "login.html", nil); err != nil { - panic(err) - } - return - } - - user, err := db.FetchUser(ctx, username) - if err != nil && err != errNoDBRows { - httpError(w, fmt.Errorf("failed to fetch user: %v", err)) - return - } - if err == nil { - err = user.VerifyPassword(password) - } - if err != nil { - log.Printf("login failed for user %q: %v", username, err) - // TODO: show error message - if err := tpl.ExecuteTemplate(w, "login.html", nil); err != nil { - panic(err) - } - return - } - - token := AccessToken{ - User: user.ID, - Scope: internalTokenScope, - } - secret, err := token.Generate() - if err != nil { - httpError(w, fmt.Errorf("failed to generate access token: %v", err)) - return - } - if err := db.CreateAccessToken(ctx, &token); err != nil { - httpError(w, fmt.Errorf("failed to create access token: %v", err)) - return - } - - setLoginTokenCookie(w, &token, secret) - http.Redirect(w, req, redirectURI.String(), http.StatusFound) - }) - - mux.HandleFunc("/authorize", func(w http.ResponseWriter, req *http.Request) { - ctx := req.Context() - - q := req.URL.Query() - respType := oauth2.ResponseType(q.Get("response_type")) - clientID := q.Get("client_id") - rawRedirectURI := q.Get("redirect_uri") - scope := q.Get("scope") - state := q.Get("state") - - if clientID == "" { - http.Error(w, "Missing client ID", http.StatusBadRequest) - return - } - - client, err := db.FetchClient(ctx, clientID) - if err == errNoDBRows { - http.Error(w, "Invalid client ID", http.StatusForbidden) - return - } else if err != nil { - httpError(w, fmt.Errorf("failed to fetch client: %v", err)) - return - } - - // TODO: validate redirect URI with client - // TODO: make redirect URI optional - redirectURI, err := url.Parse(rawRedirectURI) - if err != nil { - http.Error(w, "Invalid redirect URI", http.StatusBadRequest) - return - } - - if respType != oauth2.ResponseTypeCode { - redirectClientError(w, req, redirectURI, state, &oauth2.Error{ - Code: oauth2.ErrorCodeUnsupportedResponseType, - }) - return - } - - // TODO: add support for scope - if scope != "" { - redirectClientError(w, req, redirectURI, state, &oauth2.Error{ - Code: oauth2.ErrorCodeInvalidScope, - }) - return - } - - loginToken := loginTokenFromContext(ctx) - if loginToken == nil { - q := make(url.Values) - q.Set("redirect_uri", req.URL.String()) - u := url.URL{ - Path: "/login", - RawQuery: q.Encode(), - } - http.Redirect(w, req, u.String(), http.StatusFound) - return - } - - _ = req.ParseForm() - if _, ok := req.PostForm["authorize"]; !ok { - if err := tpl.ExecuteTemplate(w, "authorize.html", nil); err != nil { - panic(err) - } - return - } - - authCode, secret, err := NewAuthCode(loginToken.User, client.ID, scope) - if err != nil { - httpError(w, fmt.Errorf("failed to generate authentication code: %v", err)) - return - } - - if err := db.CreateAuthCode(ctx, authCode); err != nil { - httpError(w, fmt.Errorf("failed to create authentication code: %v", err)) - return - } - - code := MarshalSecret(authCode.ID, secret) - - values := make(url.Values) - values.Set("code", code) - if state != "" { - values.Set("state", state) - } - redirectClient(w, req, redirectURI, values) - }) - - mux.Post("/token", func(w http.ResponseWriter, req *http.Request) { - ctx := req.Context() - - values, err := parseRequestBody(req) - if err != nil { - oauthError(w, &oauth2.Error{ - Code: oauth2.ErrorCodeInvalidRequest, - Description: err.Error(), - }) - return - } - - clientID := values.Get("client_id") - grantType := oauth2.GrantType(values.Get("grant_type")) - scope := values.Get("scope") - - authClientID, clientSecret, _ := req.BasicAuth() - if clientID == "" && authClientID == "" { - oauthError(w, &oauth2.Error{ - Code: oauth2.ErrorCodeInvalidRequest, - Description: "Missing client ID", - }) - return - } else if clientID == "" { - clientID = authClientID - } else if clientID != authClientID { - oauthError(w, &oauth2.Error{ - Code: oauth2.ErrorCodeInvalidRequest, - Description: "Client ID in request body doesn't match Authorization header field", - }) - return - } - - client, err := db.FetchClient(ctx, clientID) - if err == errNoDBRows { - oauthError(w, &oauth2.Error{ - Code: oauth2.ErrorCodeInvalidClient, - Description: "Invalid client ID", - }) - return - } else if err != nil { - oauthError(w, fmt.Errorf("failed to fetch client: %v", err)) - return - } - - if client.ClientSecretHash != nil { - if !client.VerifySecret(clientSecret) { - oauthError(w, &oauth2.Error{ - Code: oauth2.ErrorCodeAccessDenied, - Description: "Invalid client secret", - }) - return - } - } - - if grantType != oauth2.GrantTypeAuthorizationCode { - oauthError(w, &oauth2.Error{ - Code: oauth2.ErrorCodeUnsupportedGrantType, - Description: "Unsupported grant type", - }) - return - } - - codeID, codeSecret, _ := UnmarshalSecret[*AuthCode](values.Get("code")) - authCode, err := db.PopAuthCode(ctx, codeID) - if err == errNoDBRows || (err == nil && !authCode.VerifySecret(codeSecret)) || authCode.Client != client.ID { - oauthError(w, &oauth2.Error{ - Code: oauth2.ErrorCodeAccessDenied, - Description: "Invalid authorization code", - }) - return - } else if err != nil { - oauthError(w, fmt.Errorf("failed to fetch authorization code: %v", err)) - return - } - - if scope != authCode.Scope { - oauthError(w, &oauth2.Error{ - Code: oauth2.ErrorCodeAccessDenied, - Description: "Invalid scope", - }) - return - } - - // TODO: check redirect_uri - - token, secret, err := NewAccessTokenFromAuthCode(authCode) - if err != nil { - oauthError(w, err) - return - } - - if err := db.CreateAccessToken(ctx, token); err != nil { - oauthError(w, fmt.Errorf("failed to create access token: %v", err)) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - json.NewEncoder(w).Encode(&oauth2.TokenResp{ - AccessToken: secret, - TokenType: oauth2.TokenTypeBearer, - ExpiresIn: time.Until(token.ExpiresAt), - Scope: strings.Split(token.Scope, " "), - }) - }) + mux.Get("/", index) + mux.Post("/client/new", createClient) + mux.HandleFunc("/login", login) + mux.HandleFunc("/authorize", authorize) + mux.Post("/token", exchangeToken) server := http.Server{ Addr: listenAddr, @@ -367,87 +52,7 @@ log.Fatalf("Failed to listen and serve: %v", err) } } -func parseRequestBody(req *http.Request) (url.Values, error) { - ct := req.Header.Get("Content-Type") - if ct != "" { - mimeType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) - if err != nil { - return nil, fmt.Errorf("malformed Content-Type header field") - } else if mimeType != "application/x-www-form-urlencoded" { - return nil, fmt.Errorf("unsupported request content type") - } - } - - r := io.LimitReader(req.Body, 10<<20) - b, err := io.ReadAll(r) - if err != nil { - return nil, fmt.Errorf("failed to read request body: %v", err) - } - - values, err := url.ParseQuery(string(b)) - if err != nil { - return nil, fmt.Errorf("failed to parse request body: %v", err) - } - - return values, nil -} - func httpError(w http.ResponseWriter, err error) { log.Print(err) http.Error(w, "Internal server error", http.StatusInternalServerError) } - -func oauthError(w http.ResponseWriter, err error) { - var oauthErr *oauth2.Error - if !errors.As(err, &oauthErr) { - oauthErr = &oauth2.Error{Code: oauth2.ErrorCodeServerError} - log.Print(err) - } - - statusCode := http.StatusInternalServerError - switch oauthErr.Code { - case oauth2.ErrorCodeInvalidRequest, oauth2.ErrorCodeUnsupportedResponseType, oauth2.ErrorCodeInvalidScope, oauth2.ErrorCodeInvalidClient, oauth2.ErrorCodeInvalidGrant, oauth2.ErrorCodeUnsupportedGrantType: - statusCode = http.StatusBadRequest - case oauth2.ErrorCodeUnauthorizedClient, oauth2.ErrorCodeAccessDenied: - statusCode = http.StatusForbidden - case oauth2.ErrorCodeTemporarilyUnavailable: - statusCode = http.StatusServiceUnavailable - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(oauthErr) -} - -func redirectClient(w http.ResponseWriter, req *http.Request, redirectURI *url.URL, values url.Values) { - q := redirectURI.Query() - for k, v := range values { - q[k] = v - } - - u := *redirectURI - u.RawQuery = q.Encode() - - http.Redirect(w, req, u.String(), http.StatusFound) -} - -func redirectClientError(w http.ResponseWriter, req *http.Request, redirectURI *url.URL, state string, err error) { - var oauthErr *oauth2.Error - if !errors.As(err, &oauthErr) { - oauthErr = &oauth2.Error{Code: oauth2.ErrorCodeServerError} - log.Print(err) - } - - values := make(url.Values) - values.Set("error", string(oauthErr.Code)) - if oauthErr.Description != "" { - values.Set("error_description", oauthErr.Description) - } - if oauthErr.URI != "" { - values.Set("error_uri", oauthErr.URI) - } - if state != "" { - values.Set("state", state) - } - redirectClient(w, req, redirectURI, values) -} diff --git a/oauth2.go b/oauth2.go new file mode 100644 index 0000000000000000000000000000000000000000..b0dfba011300ebc94e45f1d9847bc81a5d3c89a9 --- /dev/null +++ b/oauth2.go @@ -0,0 +1,294 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "mime" + "net/http" + "net/url" + "strings" + "time" + + "git.sr.ht/~emersion/go-oauth2" +) + +func authorize(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + db := dbFromContext(ctx) + tpl := templateFromContext(ctx) + + q := req.URL.Query() + respType := oauth2.ResponseType(q.Get("response_type")) + clientID := q.Get("client_id") + rawRedirectURI := q.Get("redirect_uri") + scope := q.Get("scope") + state := q.Get("state") + + if clientID == "" { + http.Error(w, "Missing client ID", http.StatusBadRequest) + return + } + + client, err := db.FetchClient(ctx, clientID) + if err == errNoDBRows { + http.Error(w, "Invalid client ID", http.StatusForbidden) + return + } else if err != nil { + httpError(w, fmt.Errorf("failed to fetch client: %v", err)) + return + } + + // TODO: validate redirect URI with client + // TODO: make redirect URI optional + redirectURI, err := url.Parse(rawRedirectURI) + if err != nil { + http.Error(w, "Invalid redirect URI", http.StatusBadRequest) + return + } + + if respType != oauth2.ResponseTypeCode { + redirectClientError(w, req, redirectURI, state, &oauth2.Error{ + Code: oauth2.ErrorCodeUnsupportedResponseType, + }) + return + } + + // TODO: add support for scope + if scope != "" { + redirectClientError(w, req, redirectURI, state, &oauth2.Error{ + Code: oauth2.ErrorCodeInvalidScope, + }) + return + } + + loginToken := loginTokenFromContext(ctx) + if loginToken == nil { + q := make(url.Values) + q.Set("redirect_uri", req.URL.String()) + u := url.URL{ + Path: "/login", + RawQuery: q.Encode(), + } + http.Redirect(w, req, u.String(), http.StatusFound) + return + } + + _ = req.ParseForm() + if _, ok := req.PostForm["authorize"]; !ok { + if err := tpl.ExecuteTemplate(w, "authorize.html", nil); err != nil { + panic(err) + } + return + } + + authCode, secret, err := NewAuthCode(loginToken.User, client.ID, scope) + if err != nil { + httpError(w, fmt.Errorf("failed to generate authentication code: %v", err)) + return + } + + if err := db.CreateAuthCode(ctx, authCode); err != nil { + httpError(w, fmt.Errorf("failed to create authentication code: %v", err)) + return + } + + code := MarshalSecret(authCode.ID, secret) + + values := make(url.Values) + values.Set("code", code) + if state != "" { + values.Set("state", state) + } + redirectClient(w, req, redirectURI, values) +} + +func exchangeToken(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + db := dbFromContext(ctx) + + values, err := parseRequestBody(req) + if err != nil { + oauthError(w, &oauth2.Error{ + Code: oauth2.ErrorCodeInvalidRequest, + Description: err.Error(), + }) + return + } + + clientID := values.Get("client_id") + grantType := oauth2.GrantType(values.Get("grant_type")) + scope := values.Get("scope") + + authClientID, clientSecret, _ := req.BasicAuth() + if clientID == "" && authClientID == "" { + oauthError(w, &oauth2.Error{ + Code: oauth2.ErrorCodeInvalidRequest, + Description: "Missing client ID", + }) + return + } else if clientID == "" { + clientID = authClientID + } else if clientID != authClientID { + oauthError(w, &oauth2.Error{ + Code: oauth2.ErrorCodeInvalidRequest, + Description: "Client ID in request body doesn't match Authorization header field", + }) + return + } + + client, err := db.FetchClient(ctx, clientID) + if err == errNoDBRows { + oauthError(w, &oauth2.Error{ + Code: oauth2.ErrorCodeInvalidClient, + Description: "Invalid client ID", + }) + return + } else if err != nil { + oauthError(w, fmt.Errorf("failed to fetch client: %v", err)) + return + } + + if client.ClientSecretHash != nil { + if !client.VerifySecret(clientSecret) { + oauthError(w, &oauth2.Error{ + Code: oauth2.ErrorCodeAccessDenied, + Description: "Invalid client secret", + }) + return + } + } + + if grantType != oauth2.GrantTypeAuthorizationCode { + oauthError(w, &oauth2.Error{ + Code: oauth2.ErrorCodeUnsupportedGrantType, + Description: "Unsupported grant type", + }) + return + } + + codeID, codeSecret, _ := UnmarshalSecret[*AuthCode](values.Get("code")) + authCode, err := db.PopAuthCode(ctx, codeID) + if err == errNoDBRows || (err == nil && !authCode.VerifySecret(codeSecret)) || authCode.Client != client.ID { + oauthError(w, &oauth2.Error{ + Code: oauth2.ErrorCodeAccessDenied, + Description: "Invalid authorization code", + }) + return + } else if err != nil { + oauthError(w, fmt.Errorf("failed to fetch authorization code: %v", err)) + return + } + + if scope != authCode.Scope { + oauthError(w, &oauth2.Error{ + Code: oauth2.ErrorCodeAccessDenied, + Description: "Invalid scope", + }) + return + } + + // TODO: check redirect_uri + + token, secret, err := NewAccessTokenFromAuthCode(authCode) + if err != nil { + oauthError(w, err) + return + } + + if err := db.CreateAccessToken(ctx, token); err != nil { + oauthError(w, fmt.Errorf("failed to create access token: %v", err)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + json.NewEncoder(w).Encode(&oauth2.TokenResp{ + AccessToken: secret, + TokenType: oauth2.TokenTypeBearer, + ExpiresIn: time.Until(token.ExpiresAt), + Scope: strings.Split(token.Scope, " "), + }) +} + +func parseRequestBody(req *http.Request) (url.Values, error) { + ct := req.Header.Get("Content-Type") + if ct != "" { + mimeType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) + if err != nil { + return nil, fmt.Errorf("malformed Content-Type header field") + } else if mimeType != "application/x-www-form-urlencoded" { + return nil, fmt.Errorf("unsupported request content type") + } + } + + r := io.LimitReader(req.Body, 10<<20) + b, err := io.ReadAll(r) + if err != nil { + return nil, fmt.Errorf("failed to read request body: %v", err) + } + + values, err := url.ParseQuery(string(b)) + if err != nil { + return nil, fmt.Errorf("failed to parse request body: %v", err) + } + + return values, nil +} + +func oauthError(w http.ResponseWriter, err error) { + var oauthErr *oauth2.Error + if !errors.As(err, &oauthErr) { + oauthErr = &oauth2.Error{Code: oauth2.ErrorCodeServerError} + log.Print(err) + } + + statusCode := http.StatusInternalServerError + switch oauthErr.Code { + case oauth2.ErrorCodeInvalidRequest, oauth2.ErrorCodeUnsupportedResponseType, oauth2.ErrorCodeInvalidScope, oauth2.ErrorCodeInvalidClient, oauth2.ErrorCodeInvalidGrant, oauth2.ErrorCodeUnsupportedGrantType: + statusCode = http.StatusBadRequest + case oauth2.ErrorCodeUnauthorizedClient, oauth2.ErrorCodeAccessDenied: + statusCode = http.StatusForbidden + case oauth2.ErrorCodeTemporarilyUnavailable: + statusCode = http.StatusServiceUnavailable + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(oauthErr) +} + +func redirectClient(w http.ResponseWriter, req *http.Request, redirectURI *url.URL, values url.Values) { + q := redirectURI.Query() + for k, v := range values { + q[k] = v + } + + u := *redirectURI + u.RawQuery = q.Encode() + + http.Redirect(w, req, u.String(), http.StatusFound) +} + +func redirectClientError(w http.ResponseWriter, req *http.Request, redirectURI *url.URL, state string, err error) { + var oauthErr *oauth2.Error + if !errors.As(err, &oauthErr) { + oauthErr = &oauth2.Error{Code: oauth2.ErrorCodeServerError} + log.Print(err) + } + + values := make(url.Values) + values.Set("error", string(oauthErr.Code)) + if oauthErr.Description != "" { + values.Set("error_description", oauthErr.Description) + } + if oauthErr.URI != "" { + values.Set("error_uri", oauthErr.URI) + } + if state != "" { + values.Set("state", state) + } + redirectClient(w, req, redirectURI, values) +} diff --git a/user.go b/user.go new file mode 100644 index 0000000000000000000000000000000000000000..87940917d3b207593d767d4356c284ca9124ed2c --- /dev/null +++ b/user.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "net/url" +) + +func index(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + db := dbFromContext(ctx) + tpl := templateFromContext(ctx) + + loginToken := loginTokenFromContext(ctx) + if loginToken == nil { + http.Redirect(w, req, "/login", http.StatusFound) + return + } + + clients, err := db.ListClients(ctx, loginToken.User) + if err != nil { + httpError(w, err) + return + } + + if err := tpl.ExecuteTemplate(w, "index.html", clients); err != nil { + panic(err) + } +} + +func login(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + db := dbFromContext(ctx) + tpl := templateFromContext(ctx) + + q := req.URL.Query() + rawRedirectURI := q.Get("redirect_uri") + if rawRedirectURI == "" { + rawRedirectURI = "/" + } + + redirectURI, err := url.Parse(rawRedirectURI) + if err != nil || redirectURI.Scheme != "" || redirectURI.Opaque != "" || redirectURI.User != nil || redirectURI.Host != "" { + http.Error(w, "Invalid redirect URI", http.StatusBadRequest) + return + } + + if loginTokenFromContext(ctx) != nil { + http.Redirect(w, req, redirectURI.String(), http.StatusFound) + return + } + + username := req.PostFormValue("username") + password := req.PostFormValue("password") + if username == "" { + if err := tpl.ExecuteTemplate(w, "login.html", nil); err != nil { + panic(err) + } + return + } + + user, err := db.FetchUser(ctx, username) + if err != nil && err != errNoDBRows { + httpError(w, fmt.Errorf("failed to fetch user: %v", err)) + return + } + if err == nil { + err = user.VerifyPassword(password) + } + if err != nil { + log.Printf("login failed for user %q: %v", username, err) + // TODO: show error message + if err := tpl.ExecuteTemplate(w, "login.html", nil); err != nil { + panic(err) + } + return + } + + token := AccessToken{ + User: user.ID, + Scope: internalTokenScope, + } + secret, err := token.Generate() + if err != nil { + httpError(w, fmt.Errorf("failed to generate access token: %v", err)) + return + } + if err := db.CreateAccessToken(ctx, &token); err != nil { + httpError(w, fmt.Errorf("failed to create access token: %v", err)) + return + } + + setLoginTokenCookie(w, &token, secret) + http.Redirect(w, req, redirectURI.String(), http.StatusFound) +} -- 2.48.1