package server import ( "errors" "net/http" "net/url" "strings" "time" "github.com/danyel/go-loose/internal/key" "github.com/danyel/go-loose/internal/store" ) func (s *Server) clientAuthorize(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() clientID, redirectURI, state := query.Get("client_id"), query.Get("redirect_uri"), query.Get("state") if query.Get("response_type") != "code" || clientID == "" || redirectURI == "" || state == "" { writeError(w, http.StatusBadRequest, "response_type=code, client_id, redirect_uri, and state are required") return } app, err := s.store.ValidateClientRedirect(r.Context(), clientID, redirectURI) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusBadRequest, "unknown client or redirect URI") return } s.internalError(w, "validate client redirect", err) return } if !strings.EqualFold(r.Host, s.tenantAuthHost(app.TenantSlug)) { http.Redirect(w, r, s.tenantAuthURL(app.TenantSlug, r.URL.RequestURI()), http.StatusTemporaryRedirect) return } claims, err := s.sessions.Get(r) if err != nil { returnURL := s.tenantAuthURL(app.TenantSlug, r.URL.RequestURI()) loginQuery := url.Values{"client_id": {clientID}, "return": {returnURL}} http.Redirect(w, r, "/client/login?"+loginQuery.Encode(), http.StatusSeeOther) return } app, err = s.store.ClientForAuthorization(r.Context(), clientID, redirectURI, claims.UserID) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusForbidden, "your account does not have access to this application") return } s.internalError(w, "authorize client user", err) return } code, _, codeHash, err := key.GenerateToken("glc_code_") if err != nil { s.internalError(w, "generate authorization code", err) return } if err := s.store.CreateAuthorizationCode(r.Context(), app.ID, claims.UserID, redirectURI, codeHash); err != nil { s.internalError(w, "store authorization code", err) return } target, _ := url.Parse(redirectURI) values := target.Query() values.Set("code", code) values.Set("state", state) target.RawQuery = values.Encode() http.Redirect(w, r, target.String(), http.StatusFound) } func (s *Server) clientToken(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 32<<10) if err := r.ParseForm(); err != nil { writeError(w, http.StatusBadRequest, "invalid token request") return } clientID, clientSecret, ok := r.BasicAuth() if !ok { w.Header().Set("WWW-Authenticate", `Basic realm="Go Loose token endpoint"`) writeError(w, http.StatusUnauthorized, "HTTP Basic client authentication required") return } code, redirectURI := r.Form.Get("code"), r.Form.Get("redirect_uri") if r.Form.Get("grant_type") != "authorization_code" || clientID == "" || clientSecret == "" || code == "" || redirectURI == "" { writeError(w, http.StatusBadRequest, "authorization_code grant parameters are required") return } token, _, tokenHash, err := key.GenerateToken("glc_session_") if err != nil { s.internalError(w, "generate client session", err) return } user, expiresAt, err := s.store.ExchangeAuthorizationCode( r.Context(), clientID, redirectURI, key.Hash(clientSecret), key.Hash(code), tokenHash, ) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusUnauthorized, "invalid or expired authorization code") return } s.internalError(w, "exchange authorization code", err) return } writeJSON(w, http.StatusOK, map[string]any{ "access_token": token, "token_type": "Bearer", "expires_in": int(time.Until(expiresAt).Seconds()), "user": user, }) } func (s *Server) clientUserInfo(w http.ResponseWriter, r *http.Request) { token := bearerToken(r) if token == "" { writeError(w, http.StatusUnauthorized, "Bearer token required") return } user, err := s.store.ClientUserByToken(r.Context(), key.Hash(token)) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusUnauthorized, "session is invalid or expired") return } s.internalError(w, "load client user", err) return } writeJSON(w, http.StatusOK, user) } func (s *Server) clientLogout(w http.ResponseWriter, r *http.Request) { token := bearerToken(r) if token == "" { writeError(w, http.StatusUnauthorized, "Bearer token required") return } if err := s.store.RevokeClientSession(r.Context(), key.Hash(token)); err != nil && !errors.Is(err, store.ErrNotFound) { s.internalError(w, "revoke client session", err) return } w.WriteHeader(http.StatusNoContent) } func bearerToken(r *http.Request) string { value := r.Header.Get("Authorization") if !strings.HasPrefix(value, "Bearer ") { return "" } return strings.TrimSpace(strings.TrimPrefix(value, "Bearer ")) }