// mock-oauth is a minimal OAuth2 server for local development and testing. // It mimics Google/GitHub OAuth2 Authorization Code flow so you can test // login without real OAuth provider credentials. // // Flow: // 1. Frontend gets auth URL from your backend (POST /oauth/redirect-url with provider=mock) // 2. User is redirected to this mock's /authorize // 3. User clicks "Login" → mock redirects to your frontend's redirect_uri with ?code=...&state=... // 4. Frontend sends code to your backend (POST /oauth/callback) // 5. Backend exchanges code for token at this mock's /token, gets user at /userinfo // // Run: go run cmd/mock-oauth/main.go // Default: http://localhost:9999 package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" "strings" ) const ( defaultPort = "9999" mockCode = "mock_auth_code_12345" mockAccessToken = "mock_access_token_67890" mockEmail = "dev@example.com" mockName = "Dev User" mockGivenName = "Dev" mockFamilyName = "User" mockID = "mock-user-001" ) func main() { port := defaultPort if p := strings.TrimSpace(os.Getenv("PORT")); p != "" { port = p } http.HandleFunc("/authorize", handleAuthorize) http.HandleFunc("/token", handleToken) http.HandleFunc("/userinfo", handleUserinfo) http.HandleFunc("/", handleRoot) addr := ":" + port log.Printf("Mock OAuth server running at http://localhost%s", addr) log.Printf(" /authorize - OAuth2 authorize (redirect_uri, state, client_id)") log.Printf(" /token - OAuth2 token exchange") log.Printf(" /userinfo - User info (Bearer token)") log.Printf("") log.Printf("Configure your app: oauth.mock.base_url=http://localhost%s", addr) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatal(err) } } func handleRoot(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/plain") _, _ = w.Write([]byte("Mock OAuth2 Server\n\nEndpoints: /authorize, /token, /userinfo")) } func handleAuthorize(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } _ = r.ParseForm() redirectURI := r.FormValue("redirect_uri") if redirectURI == "" { redirectURI = r.URL.Query().Get("redirect_uri") } state := r.FormValue("state") if state == "" { state = r.URL.Query().Get("state") } clientID := r.FormValue("client_id") if clientID == "" { clientID = r.URL.Query().Get("client_id") } if redirectURI == "" { http.Error(w, "redirect_uri is required", http.StatusBadRequest) return } if r.FormValue("approve") != "" || r.FormValue("login") != "" { redir, _ := urlAddQuery(redirectURI, map[string]string{ "code": mockCode, "state": state, }) http.Redirect(w, r, redir, http.StatusFound) return } // Show simple login page w.Header().Set("Content-Type", "text/html; charset=utf-8") html := fmt.Sprintf(` Mock OAuth Login

Mock OAuth2

Local development login. Client: %s

`, escapeHTML(clientID), escapeHTML(redirectURI), escapeHTML(state), escapeHTML(clientID)) _, _ = w.Write([]byte(html)) } func urlAddQuery(base string, params map[string]string) (string, error) { u, err := url.Parse(base) if err != nil { return base, err } q := u.Query() for k, v := range params { if v != "" { q.Set(k, v) } } u.RawQuery = q.Encode() return u.String(), nil } func escapeHTML(s string) string { s = strings.ReplaceAll(s, "&", "&") s = strings.ReplaceAll(s, "<", "<") s = strings.ReplaceAll(s, ">", ">") s = strings.ReplaceAll(s, "\"", """) return s } func handleToken(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } _ = r.ParseForm() code := r.FormValue("code") grantType := r.FormValue("grant_type") if grantType != "authorization_code" || code != mockCode { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]string{ "error": "invalid_grant", "error_description": "invalid code or grant_type", }) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]interface{}{ "access_token": mockAccessToken, "token_type": "Bearer", "expires_in": 3600, "refresh_token": "mock_refresh_token", }) } func handleUserinfo(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") { http.Error(w, "missing or invalid Authorization", http.StatusUnauthorized) return } token := strings.TrimPrefix(auth, "Bearer ") if token != mockAccessToken { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(map[string]string{"error": "invalid_token"}) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{ "id": mockID, "email": mockEmail, "name": mockName, "given_name": mockGivenName, "family_name": mockFamilyName, }) }