+2
-1
appview/config/config.go
+2
-1
appview/config/config.go
+3
-13
appview/oauth/handler.go
+3
-13
appview/oauth/handler.go
···
12
13
"github.com/bluesky-social/indigo/atproto/auth/oauth"
14
"github.com/go-chi/chi/v5"
15
-
"github.com/lestrrat-go/jwx/v2/jwk"
16
"github.com/posthog/posthog-go"
17
"tangled.org/core/api/tangled"
18
"tangled.org/core/appview/db"
···
41
}
42
43
func (o *OAuth) jwks(w http.ResponseWriter, r *http.Request) {
44
-
jwks := o.Config.OAuth.Jwks
45
-
pubKey, err := pubKeyFromJwk(jwks)
46
-
if err != nil {
47
-
o.Logger.Error("error parsing public key", "err", err)
48
http.Error(w, err.Error(), http.StatusInternalServerError)
49
return
50
}
51
-
52
-
response := map[string]any{
53
-
"keys": []jwk.Key{pubKey},
54
-
}
55
-
56
-
w.Header().Set("Content-Type", "application/json")
57
-
w.WriteHeader(http.StatusOK)
58
-
json.NewEncoder(w).Encode(response)
59
}
60
61
func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) {
···
12
13
"github.com/bluesky-social/indigo/atproto/auth/oauth"
14
"github.com/go-chi/chi/v5"
15
"github.com/posthog/posthog-go"
16
"tangled.org/core/api/tangled"
17
"tangled.org/core/appview/db"
···
40
}
41
42
func (o *OAuth) jwks(w http.ResponseWriter, r *http.Request) {
43
+
w.Header().Set("Content-Type", "application/json")
44
+
body := o.ClientApp.Config.PublicJWKS()
45
+
if err := json.NewEncoder(w).Encode(body); err != nil {
46
http.Error(w, err.Error(), http.StatusInternalServerError)
47
return
48
}
49
}
50
51
func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) {
+10
-13
appview/oauth/oauth.go
+10
-13
appview/oauth/oauth.go
···
10
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
"github.com/bluesky-social/indigo/atproto/auth/oauth"
12
atpclient "github.com/bluesky-social/indigo/atproto/client"
13
"github.com/bluesky-social/indigo/atproto/syntax"
14
xrpc "github.com/bluesky-social/indigo/xrpc"
15
"github.com/gorilla/sessions"
16
-
"github.com/lestrrat-go/jwx/v2/jwk"
17
"github.com/posthog/posthog-go"
18
"tangled.org/core/appview/config"
19
"tangled.org/core/appview/db"
···
47
clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri)
48
callbackUri := clientUri + "/oauth/callback"
49
oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, []string{"atproto", "transition:generic"})
50
}
51
52
jwksUri := clientUri + "/oauth/jwks.json"
···
138
err2 := o.SessStore.Save(r, w, userSession)
139
140
return errors.Join(err1, err2)
141
-
}
142
-
143
-
func pubKeyFromJwk(jwks string) (jwk.Key, error) {
144
-
k, err := jwk.ParseKey([]byte(jwks))
145
-
if err != nil {
146
-
return nil, err
147
-
}
148
-
pubKey, err := k.PublicKey()
149
-
if err != nil {
150
-
return nil, err
151
-
}
152
-
return pubKey, nil
153
}
154
155
type User struct {
···
10
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
"github.com/bluesky-social/indigo/atproto/auth/oauth"
12
atpclient "github.com/bluesky-social/indigo/atproto/client"
13
+
atcrypto "github.com/bluesky-social/indigo/atproto/crypto"
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
xrpc "github.com/bluesky-social/indigo/xrpc"
16
"github.com/gorilla/sessions"
17
"github.com/posthog/posthog-go"
18
"tangled.org/core/appview/config"
19
"tangled.org/core/appview/db"
···
47
clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri)
48
callbackUri := clientUri + "/oauth/callback"
49
oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, []string{"atproto", "transition:generic"})
50
+
}
51
+
52
+
// configure client secret
53
+
priv, err := atcrypto.ParsePrivateMultibase(config.OAuth.ClientSecret)
54
+
if err != nil {
55
+
return nil, err
56
+
}
57
+
if err := oauthConfig.SetClientSecret(priv, config.OAuth.ClientKid); err != nil {
58
+
return nil, err
59
}
60
61
jwksUri := clientUri + "/oauth/jwks.json"
···
147
err2 := o.SessStore.Save(r, w, userSession)
148
149
return errors.Join(err1, err2)
150
}
151
152
type User struct {
-43
cmd/genjwks/main.go
-43
cmd/genjwks/main.go
···
1
-
// adapted from https://tangled.org/anirudh.fi/atproto-oauth
2
-
3
-
package main
4
-
5
-
import (
6
-
"crypto/ecdsa"
7
-
"crypto/elliptic"
8
-
"crypto/rand"
9
-
"encoding/json"
10
-
"fmt"
11
-
"time"
12
-
13
-
"github.com/lestrrat-go/jwx/v2/jwk"
14
-
)
15
-
16
-
func main() {
17
-
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
18
-
if err != nil {
19
-
panic(err)
20
-
}
21
-
22
-
key, err := jwk.FromRaw(privKey)
23
-
if err != nil {
24
-
panic(err)
25
-
}
26
-
27
-
kid := fmt.Sprintf("%d", time.Now().Unix())
28
-
29
-
if err := key.Set(jwk.KeyIDKey, kid); err != nil {
30
-
panic(err)
31
-
}
32
-
33
-
if err := key.Set("use", "sig"); err != nil {
34
-
panic(err)
35
-
}
36
-
37
-
b, err := json.Marshal(key)
38
-
if err != nil {
39
-
panic(err)
40
-
}
41
-
42
-
fmt.Println(string(b))
43
-
}
···
+14
-4
docs/hacking.md
+14
-4
docs/hacking.md
···
37
38
```
39
# oauth jwks should already be setup by the nix devshell:
40
-
echo $TANGLED_OAUTH_JWKS
41
-
{"crv":"P-256","d":"tELKHYH-Dko6qo4ozYcVPE1ah6LvXHFV2wpcWpi8ab4","kid":"1753352226","kty":"EC","x":"mRzYpLzAGq74kJez9UbgGfV040DxgsXpMbaVsdy8RZs","y":"azqqXzUYywMlLb2Uc5AVG18nuLXyPnXr4kI4T39eeIc"}
42
43
# if not, you can set it up yourself:
44
-
go build -o genjwks.out ./cmd/genjwks
45
-
export TANGLED_OAUTH_JWKS="$(./genjwks.out)"
46
47
# run redis in at a new shell to store oauth sessions
48
redis-server
···
37
38
```
39
# oauth jwks should already be setup by the nix devshell:
40
+
echo $TANGLED_OAUTH_CLIENT_SECRET
41
+
z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc
42
+
43
+
echo $TANGLED_OAUTH_CLIENT_KID
44
+
1761667908
45
46
# if not, you can set it up yourself:
47
+
goat key generate -t P-256
48
+
Key Type: P-256 / secp256r1 / ES256 private key
49
+
Secret Key (Multibase Syntax): save this securely (eg, add to password manager)
50
+
z42tuPDKRfM2mz2Kv953ARen2jmrPA8S9LX9tRq4RVcUMwwL
51
+
Public Key (DID Key Syntax): share or publish this (eg, in DID document)
52
+
did:key:zDnaeUBxtG6Xuv3ATJE4GaWeyXM3jyamJsZw3bSPpxx4bNXDR
53
+
54
+
# the secret key from above
55
+
export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..."
56
57
# run redis in at a new shell to store oauth sessions
58
redis-server
+5
-4
flake.nix
+5
-4
flake.nix
···
78
inherit (pkgs) gcc;
79
inherit sqlite-lib-src;
80
};
81
-
genjwks = self.callPackage ./nix/pkgs/genjwks.nix {};
82
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
83
appview-static-files = self.callPackage ./nix/pkgs/appview-static-files.nix {
84
inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src;
85
};
···
90
});
91
in {
92
overlays.default = final: prev: {
93
-
inherit (mkPackageSet final) lexgen sqlite-lib genjwks spindle knot-unwrapped knot appview;
94
};
95
96
packages = forAllSystems (system: let
···
99
staticPackages = mkPackageSet pkgs.pkgsStatic;
100
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
101
in {
102
-
inherit (packages) appview appview-static-files lexgen genjwks spindle knot knot-unwrapped sqlite-lib;
103
104
pkgsStatic-appview = staticPackages.appview;
105
pkgsStatic-knot = staticPackages.knot;
···
167
mkdir -p appview/pages/static
168
# no preserve is needed because watch-tailwind will want to be able to overwrite
169
cp -fr --no-preserve=ownership ${packages'.appview-static-files}/* appview/pages/static
170
-
export TANGLED_OAUTH_JWKS="$(${packages'.genjwks}/bin/genjwks)"
171
'';
172
env.CGO_ENABLED = 1;
173
};
···
78
inherit (pkgs) gcc;
79
inherit sqlite-lib-src;
80
};
81
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
82
+
goat = self.callPackage ./nix/pkgs/goat.nix {inherit indigo;};
83
appview-static-files = self.callPackage ./nix/pkgs/appview-static-files.nix {
84
inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src;
85
};
···
90
});
91
in {
92
overlays.default = final: prev: {
93
+
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview;
94
};
95
96
packages = forAllSystems (system: let
···
99
staticPackages = mkPackageSet pkgs.pkgsStatic;
100
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
101
in {
102
+
inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib;
103
104
pkgsStatic-appview = staticPackages.appview;
105
pkgsStatic-knot = staticPackages.knot;
···
167
mkdir -p appview/pages/static
168
# no preserve is needed because watch-tailwind will want to be able to overwrite
169
cp -fr --no-preserve=ownership ${packages'.appview-static-files}/* appview/pages/static
170
+
export TANGLED_OAUTH_CLIENT_KID="$(date +%s)"
171
+
export TANGLED_OAUTH_CLIENT_SECRET="$(${packages'.goat}/bin/goat key generate -t P-256 | grep -A1 "Secret Key" | tail -n1 | awk '{print $1}')"
172
'';
173
env.CGO_ENABLED = 1;
174
};
-18
nix/pkgs/genjwks.nix
-18
nix/pkgs/genjwks.nix
···
1
-
{
2
-
buildGoApplication,
3
-
modules,
4
-
}:
5
-
buildGoApplication {
6
-
pname = "genjwks";
7
-
version = "0.1.0";
8
-
src = ../../cmd/genjwks;
9
-
postPatch = ''
10
-
ln -s ${../../go.mod} ./go.mod
11
-
'';
12
-
postInstall = ''
13
-
mv $out/bin/core $out/bin/genjwks
14
-
'';
15
-
inherit modules;
16
-
doCheck = false;
17
-
CGO_ENABLED = 0;
18
-
}
···
+12
nix/pkgs/goat.nix
+12
nix/pkgs/goat.nix
-26
scripts/appview.sh
-26
scripts/appview.sh
···
1
-
#!/bin/bash
2
-
3
-
# Variables
4
-
BINARY_NAME="appview"
5
-
BINARY_PATH=".bin/app"
6
-
SERVER="95.111.206.63"
7
-
USER="appview"
8
-
9
-
# SCP the binary to root's home directory
10
-
scp "$BINARY_PATH" root@$SERVER:/root/"$BINARY_NAME"
11
-
12
-
# SSH into the server and perform the necessary operations
13
-
ssh root@$SERVER <<EOF
14
-
set -e # Exit on error
15
-
16
-
# Move binary to /usr/local/bin and set executable permissions
17
-
mv /root/$BINARY_NAME /usr/local/bin/$BINARY_NAME
18
-
chmod +x /usr/local/bin/$BINARY_NAME
19
-
20
-
su appview
21
-
cd ~
22
-
./reset.sh
23
-
EOF
24
-
25
-
echo "Deployment complete."
26
-
···