tangled
alpha
login
or
join now
margin.at
/
margin
87
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
87
fork
atom
overview
issues
4
pulls
1
pipelines
Introducing margin.cafe PDS
scanash.com
1 month ago
2c4e898c
7b9d4d88
+70
-18
6 changed files
expand all
collapse all
unified
split
backend
go.mod
internal
oauth
client.go
handler.go
web
src
components
Icons.jsx
SignUpModal.jsx
pages
Login.jsx
+3
-3
backend/go.mod
···
3
3
go 1.24.0
4
4
5
5
require (
6
6
+
github.com/fxamacker/cbor/v2 v2.9.0
6
7
github.com/go-chi/chi/v5 v5.1.0
7
8
github.com/go-chi/cors v1.2.1
8
9
github.com/go-jose/go-jose/v4 v4.0.4
9
10
github.com/gorilla/websocket v1.5.3
11
11
+
github.com/ipfs/go-cid v0.6.0
10
12
github.com/joho/godotenv v1.5.1
11
13
github.com/lib/pq v1.10.9
12
14
github.com/mattn/go-sqlite3 v1.14.22
15
15
+
github.com/multiformats/go-multihash v0.2.3
13
16
golang.org/x/image v0.34.0
14
17
)
15
18
16
19
require (
17
20
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
18
18
-
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
19
19
-
github.com/ipfs/go-cid v0.6.0 // indirect
20
21
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
21
22
github.com/minio/sha256-simd v1.0.0 // indirect
22
23
github.com/mr-tron/base58 v1.2.0 // indirect
23
24
github.com/multiformats/go-base32 v0.0.3 // indirect
24
25
github.com/multiformats/go-base36 v0.1.0 // indirect
25
26
github.com/multiformats/go-multibase v0.2.0 // indirect
26
26
-
github.com/multiformats/go-multihash v0.2.3 // indirect
27
27
github.com/multiformats/go-varint v0.1.0 // indirect
28
28
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
29
29
github.com/spaolacci/murmur3 v1.1.0 // indirect
+10
-3
backend/internal/oauth/client.go
···
311
311
}
312
312
313
313
func (c *Client) SendPAR(meta *AuthServerMetadata, loginHint, scope string, dpopKey *ecdsa.PrivateKey, pkceChallenge string) (*PARResponse, string, string, error) {
314
314
+
return c.SendPARWithPrompt(meta, loginHint, scope, dpopKey, pkceChallenge, "")
315
315
+
}
316
316
+
317
317
+
func (c *Client) SendPARWithPrompt(meta *AuthServerMetadata, loginHint, scope string, dpopKey *ecdsa.PrivateKey, pkceChallenge, prompt string) (*PARResponse, string, string, error) {
314
318
stateBytes := make([]byte, 16)
315
319
rand.Read(stateBytes)
316
320
state := base64.RawURLEncoding.EncodeToString(stateBytes)
317
321
318
318
-
parResp, dpopNonce, err := c.sendPARRequest(meta, loginHint, scope, dpopKey, pkceChallenge, state, "")
322
322
+
parResp, dpopNonce, err := c.sendPARRequest(meta, loginHint, scope, dpopKey, pkceChallenge, state, "", prompt)
319
323
if err != nil {
320
324
321
325
if strings.Contains(err.Error(), "use_dpop_nonce") && dpopNonce != "" {
322
326
323
323
-
parResp, dpopNonce, err = c.sendPARRequest(meta, loginHint, scope, dpopKey, pkceChallenge, state, dpopNonce)
327
327
+
parResp, dpopNonce, err = c.sendPARRequest(meta, loginHint, scope, dpopKey, pkceChallenge, state, dpopNonce, prompt)
324
328
if err != nil {
325
329
return nil, "", "", err
326
330
}
···
332
336
return parResp, state, dpopNonce, nil
333
337
}
334
338
335
335
-
func (c *Client) sendPARRequest(meta *AuthServerMetadata, loginHint, scope string, dpopKey *ecdsa.PrivateKey, pkceChallenge, state, dpopNonce string) (*PARResponse, string, error) {
339
339
+
func (c *Client) sendPARRequest(meta *AuthServerMetadata, loginHint, scope string, dpopKey *ecdsa.PrivateKey, pkceChallenge, state, dpopNonce, prompt string) (*PARResponse, string, error) {
336
340
dpopProof, err := c.CreateDPoPProof(dpopKey, "POST", meta.PushedAuthorizationRequestEndpoint, dpopNonce, "")
337
341
if err != nil {
338
342
return nil, "", err
···
355
359
data.Set("client_assertion", clientAssertion)
356
360
if loginHint != "" {
357
361
data.Set("login_hint", loginHint)
362
362
+
}
363
363
+
if prompt != "" {
364
364
+
data.Set("prompt", prompt)
358
365
}
359
366
360
367
req, err := http.NewRequest("POST", meta.PushedAuthorizationRequestEndpoint, strings.NewReader(data.Encode()))
+14
-6
backend/internal/oauth/handler.go
···
13
13
"net/http"
14
14
"net/url"
15
15
"os"
16
16
+
"strings"
16
17
"sync"
17
18
"time"
18
19
···
325
326
pkceVerifier, pkceChallenge := client.GeneratePKCE()
326
327
scope := "atproto offline_access blob:* include:at.margin.authFull"
327
328
328
328
-
parResp, state, dpopNonce, err := client.SendPAR(meta, "", scope, dpopKey, pkceChallenge)
329
329
+
parResp, state, dpopNonce, err := client.SendPARWithPrompt(meta, "", scope, dpopKey, pkceChallenge, "create")
329
330
if err != nil {
330
330
-
log.Printf("PAR request failed for signup: %v", err)
331
331
-
w.Header().Set("Content-Type", "application/json")
332
332
-
w.WriteHeader(http.StatusInternalServerError)
333
333
-
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to initiate signup"})
334
334
-
return
331
331
+
if strings.Contains(err.Error(), "prompt") || strings.Contains(err.Error(), "invalid_request") {
332
332
+
log.Printf("prompt=create not supported, falling back to standard flow")
333
333
+
pkceVerifier, pkceChallenge = client.GeneratePKCE()
334
334
+
parResp, state, dpopNonce, err = client.SendPAR(meta, "", scope, dpopKey, pkceChallenge)
335
335
+
}
336
336
+
if err != nil {
337
337
+
log.Printf("PAR request failed for signup: %v", err)
338
338
+
w.Header().Set("Content-Type", "application/json")
339
339
+
w.WriteHeader(http.StatusInternalServerError)
340
340
+
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to initiate signup"})
341
341
+
return
342
342
+
}
335
343
}
336
344
337
345
pending := &PendingAuth{
+15
web/src/components/Icons.jsx
···
282
282
);
283
283
}
284
284
285
285
+
export function MarginIcon({ size = 18 }) {
286
286
+
return (
287
287
+
<svg
288
288
+
width={size}
289
289
+
height={size}
290
290
+
viewBox="0 0 265 231"
291
291
+
fill="currentColor"
292
292
+
xmlns="http://www.w3.org/2000/svg"
293
293
+
>
294
294
+
<path d="M0 230 V0 H199 V65.7156 H149.5 V115.216 H182.5 L199 131.716 V230 Z" />
295
295
+
<path d="M215 214.224 V230 H264.5 V0 H215.07 V16.2242 H248.5 V214.224 H215 Z" />
296
296
+
</svg>
297
297
+
);
298
298
+
}
299
299
+
285
300
export function LogoutIcon({ size = 18 }) {
286
301
return (
287
302
<svg
+27
-6
web/src/components/SignUpModal.jsx
···
1
1
import { useState, useEffect } from "react";
2
2
import { X, ChevronRight, Loader2, AlertCircle } from "lucide-react";
3
3
-
import { BlackskyIcon, NorthskyIcon, BlueskyIcon, TophhieIcon } from "./Icons";
3
3
+
import {
4
4
+
BlackskyIcon,
5
5
+
NorthskyIcon,
6
6
+
BlueskyIcon,
7
7
+
TophhieIcon,
8
8
+
MarginIcon,
9
9
+
} from "./Icons";
4
10
import { startSignup } from "../api/client";
5
11
6
12
const RECOMMENDED_PROVIDER = {
7
7
-
id: "bluesky",
8
8
-
name: "Bluesky",
9
9
-
service: "https://bsky.social",
10
10
-
Icon: BlueskyIcon,
11
11
-
description: "The most popular option, recommended for most people",
13
13
+
id: "margin",
14
14
+
name: "Margin",
15
15
+
service: "https://margin.cafe",
16
16
+
Icon: MarginIcon,
17
17
+
description: "Hosted by Margin, the easiest way to get started",
12
18
};
13
19
14
20
const OTHER_PROVIDERS = [
21
21
+
{
22
22
+
id: "bluesky",
23
23
+
name: "Bluesky",
24
24
+
service: "https://bsky.social",
25
25
+
Icon: BlueskyIcon,
26
26
+
description: "The most popular option on the AT Protocol",
27
27
+
},
15
28
{
16
29
id: "blacksky",
17
30
name: "Blacksky",
18
31
service: "https://blacksky.app",
19
32
Icon: BlackskyIcon,
20
33
description: "For the Culture. A safe space for Black users and allies",
34
34
+
},
35
35
+
{
36
36
+
id: "selfhosted.social",
37
37
+
name: "selfhosted.social",
38
38
+
service: "https://selfhosted.social",
39
39
+
Icon: null,
40
40
+
description:
41
41
+
"For hackers, designers, developers, ATProto enthusiasts, scrobblers, tinkerers, friends, and curious minds.",
21
42
},
22
43
{
23
44
id: "northsky",
+1
web/src/pages/Login.jsx
···
25
25
const [morphClass, setMorphClass] = useState("morph-in");
26
26
const providers = [
27
27
"AT Protocol",
28
28
+
"Margin",
28
29
"Bluesky",
29
30
"Blacksky",
30
31
"Tangled",