tangled
alpha
login
or
join now
stream.place
/
streamplace
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
oproxy: implemented PAR
Eli Mallon
9 months ago
72edee6a
29811731
+247
-211
8 changed files
expand all
collapse all
unified
split
pkg
atproto
client_metadata.go
oproxy
handlers.go
helpers.go
oauth_downstream.go
oauth_session.go
oauth_upstream.go
oproxy.go
par.go
-55
pkg/atproto/client_metadata.go
···
1
1
package atproto
2
2
3
3
-
import (
4
4
-
"fmt"
5
5
-
)
6
6
-
7
3
var AllowedPlatforms = []string{"ios", "android", "web"}
8
4
9
5
type OAuthClientMetadata struct {
···
37
33
AuthorizationDetailsTypes []string `json:"authorization_details_types,omitempty"`
38
34
// Jwks *JWKSet `json:"jwks,omitempty"` // You'll need to define JWKSet type
39
35
}
40
40
-
41
41
-
func boolPtr(b bool) *bool {
42
42
-
return &b
43
43
-
}
44
44
-
45
45
-
func GetUpstreamMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata {
46
46
-
meta := &OAuthClientMetadata{
47
47
-
ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/upstream/%s", host, platform),
48
48
-
JwksURI: fmt.Sprintf("https://%s/api/atproto-oauth/jwks.json", host),
49
49
-
ClientURI: fmt.Sprintf("https://%s", host),
50
50
-
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
51
51
-
Scope: "atproto transition:generic",
52
52
-
TokenEndpointAuthMethod: "private_key_jwt",
53
53
-
ClientName: "Streamplace",
54
54
-
ResponseTypes: []string{"code"},
55
55
-
GrantTypes: []string{"authorization_code", "refresh_token"},
56
56
-
DPoPBoundAccessTokens: boolPtr(true),
57
57
-
TokenEndpointAuthSigningAlg: "ES256",
58
58
-
}
59
59
-
60
60
-
if platform == "web" {
61
61
-
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/api/oauth/return", host)}
62
62
-
meta.ApplicationType = "web"
63
63
-
} else {
64
64
-
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/api/app-return/%s", host, appBundleId)}
65
65
-
meta.ApplicationType = "native"
66
66
-
}
67
67
-
return meta
68
68
-
}
69
69
-
70
70
-
func GetDownstreamMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata {
71
71
-
meta := &OAuthClientMetadata{
72
72
-
ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/downstream/%s", host, platform),
73
73
-
ClientURI: fmt.Sprintf("https://%s", host),
74
74
-
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
75
75
-
Scope: "atproto transition:generic",
76
76
-
TokenEndpointAuthMethod: "none",
77
77
-
ClientName: "Streamplace",
78
78
-
ResponseTypes: []string{"code"},
79
79
-
GrantTypes: []string{"authorization_code", "refresh_token"},
80
80
-
DPoPBoundAccessTokens: boolPtr(true),
81
81
-
}
82
82
-
if platform == "web" {
83
83
-
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/login", host)}
84
84
-
meta.ApplicationType = "web"
85
85
-
} else {
86
86
-
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/api/app-return/%s", host, appBundleId)}
87
87
-
meta.ApplicationType = "native"
88
88
-
}
89
89
-
return meta
90
90
-
}
+25
-128
pkg/oproxy/handlers.go
···
2
2
3
3
import (
4
4
"encoding/json"
5
5
-
"errors"
6
5
"fmt"
7
7
-
"net"
8
6
"net/http"
9
7
"net/url"
10
10
-
"slices"
11
8
"time"
12
9
13
13
-
"github.com/AxisCommunications/go-dpop"
14
10
"github.com/labstack/echo/v4"
15
11
"go.opentelemetry.io/otel"
16
12
"stream.place/streamplace/pkg/atproto"
17
13
"stream.place/streamplace/pkg/log"
18
18
-
"stream.place/streamplace/pkg/model"
19
14
)
20
15
21
16
func (o *OProxy) Handler() http.Handler {
···
26
21
o.e.GET("/oauth/return", o.HandleOAuthReturn)
27
22
o.e.POST("/oauth/token", o.HandleOAuthToken)
28
23
o.e.POST("/oauth/revoke", o.HandleOAuthRevoke)
29
29
-
o.e.GET("/oauth/upstream/client-metadata.json", o.HandleATProtoOAuthUpstream)
30
30
-
o.e.GET("/oauth/downstream/client-metadata.json", o.HandleATProtoOAuthDownstream)
24
24
+
o.e.GET("/oauth/upstream/client-metadata.json", o.HandleClientMetadataUpstream)
25
25
+
o.e.GET("/oauth/downstream/client-metadata.json", o.HandleClientMetadataDownstream)
31
26
// prefer to handle this by returning in the metadata blob:
32
27
// apiRouter.GET("/api/atproto-oauth/jwks.json", a.HandleJWKPublic(ctx))
33
28
return o.e
34
29
}
35
30
36
31
func (o *OProxy) HandleOAuthAuthorizationServer(c echo.Context) error {
37
37
-
w.Header().Set("Access-Control-Allow-Origin", "*")
38
38
-
w.Header().Set("Content-Type", "application/json")
39
39
-
w.WriteHeader(200)
40
40
-
json.NewEncoder(w).Encode(generateOAuthServerMetadata("longos.iameli.link"))
32
32
+
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
33
33
+
c.Response().Header().Set("Content-Type", "application/json")
34
34
+
c.Response().WriteHeader(200)
35
35
+
json.NewEncoder(c.Response().Writer).Encode(generateOAuthServerMetadata("longos.iameli.link"))
36
36
+
return nil
41
37
}
42
38
43
43
-
func (o *OProxy) HandleATProtoOAuthUpstream(c echo.Context) error {
44
44
-
host, _, err := net.SplitHostPort(req.Host)
45
45
-
if err != nil {
46
46
-
host = req.Host
47
47
-
}
48
48
-
if !slices.Contains(atproto.AllowedPlatforms, platform) {
49
49
-
apierrors.WriteHTTPBadRequest(w, "unsupported platform", nil)
50
50
-
return
51
51
-
}
52
52
-
53
53
-
meta := atproto.GetUpstreamMetadata(host, platform, a.CLI.AppBundleID)
54
54
-
bs, err := json.Marshal(meta)
55
55
-
if err != nil {
56
56
-
apierrors.WriteHTTPInternalServerError(w, "could not marshal metadata", err)
57
57
-
return
58
58
-
}
59
59
-
w.Header().Set("Content-Type", "application/json")
60
60
-
w.Write(bs)
39
39
+
func (o *OProxy) HandleClientMetadataUpstream(c echo.Context) error {
40
40
+
meta := o.GetUpstreamMetadata()
41
41
+
return c.JSON(200, meta)
61
42
}
62
43
63
63
-
func (o *OProxy) HandleATProtoOAuthDownstream(c echo.Context) error {
64
64
-
host, _, err := net.SplitHostPort(req.Host)
65
65
-
if err != nil {
66
66
-
host = req.Host
67
67
-
}
68
68
-
if !slices.Contains(atproto.AllowedPlatforms, platform) {
69
69
-
apierrors.WriteHTTPBadRequest(w, "unsupported platform", nil)
70
70
-
return
71
71
-
}
72
72
-
73
73
-
meta := atproto.GetDownstreamMetadata(host, platform, a.CLI.AppBundleID)
74
74
-
bs, err := json.Marshal(meta)
75
75
-
if err != nil {
76
76
-
apierrors.WriteHTTPInternalServerError(w, "could not marshal metadata", err)
77
77
-
return
78
78
-
}
79
79
-
w.Header().Set("Content-Type", "application/json")
80
80
-
w.Write(bs)
81
81
-
}
82
82
-
83
83
-
func generateOAuthServerMetadata(host string) map[string]any {
84
84
-
oauthServerMetadata := map[string]any{
85
85
-
"issuer": fmt.Sprintf("https://%s", host),
86
86
-
"request_parameter_supported": true,
87
87
-
"request_uri_parameter_supported": true,
88
88
-
"require_request_uri_registration": true,
89
89
-
"scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"},
90
90
-
"subject_types_supported": []string{"public"},
91
91
-
"response_types_supported": []string{"code"},
92
92
-
"response_modes_supported": []string{"query", "fragment", "form_post"},
93
93
-
"grant_types_supported": []string{"authorization_code", "refresh_token"},
94
94
-
"code_challenge_methods_supported": []string{"S256"},
95
95
-
"ui_locales_supported": []string{"en-US"},
96
96
-
"display_values_supported": []string{"page", "popup", "touch"},
97
97
-
"authorization_response_iss_parameter_supported": true,
98
98
-
"request_object_encryption_alg_values_supported": []string{},
99
99
-
"request_object_encryption_enc_values_supported": []string{},
100
100
-
"jwks_uri": fmt.Sprintf("https://%s/api/oauth/jwks", host),
101
101
-
"authorization_endpoint": fmt.Sprintf("https://%s/api/oauth/authorize", host),
102
102
-
"token_endpoint": fmt.Sprintf("https://%s/api/oauth/token", host),
103
103
-
"token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"},
104
104
-
"revocation_endpoint": fmt.Sprintf("https://%s/api/oauth/revoke", host),
105
105
-
"introspection_endpoint": fmt.Sprintf("https://%s/api/oauth/introspect", host),
106
106
-
"pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/api/oauth/par", host),
107
107
-
"require_pushed_authorization_requests": true,
108
108
-
"client_id_metadata_document_supported": true,
109
109
-
"request_object_signing_alg_values_supported": []string{
110
110
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
111
111
-
"ES256", "ES256K", "ES384", "ES512", "none",
112
112
-
},
113
113
-
"token_endpoint_auth_signing_alg_values_supported": []string{
114
114
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
115
115
-
"ES256", "ES256K", "ES384", "ES512",
116
116
-
},
117
117
-
"dpop_signing_alg_values_supported": []string{
118
118
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
119
119
-
"ES256", "ES256K", "ES384", "ES512",
120
120
-
},
121
121
-
}
122
122
-
return oauthServerMetadata
44
44
+
func (o *OProxy) HandleClientMetadataDownstream(c echo.Context) error {
45
45
+
meta := o.GetDownstreamMetadata()
46
46
+
return c.JSON(200, meta)
123
47
}
124
48
125
49
func (o *OProxy) HandleOAuthProtectedResource(c echo.Context) error {
126
126
-
w.Header().Set("Access-Control-Allow-Origin", "*")
127
127
-
w.Header().Set("Content-Type", "application/json")
128
128
-
w.WriteHeader(200)
129
129
-
json.NewEncoder(w).Encode(map[string]interface{}{
130
130
-
"resource": "https://longos.iameli.link",
50
50
+
return c.JSON(200, map[string]interface{}{
51
51
+
"resource": fmt.Sprintf("https://%s", o.host),
131
52
"authorization_servers": []string{
132
132
-
"https://longos.iameli.link",
53
53
+
fmt.Sprintf("https://%s", o.host),
133
54
},
134
55
"scopes_supported": []string{},
135
56
"bearer_methods_supported": []string{
···
137
58
},
138
59
"resource_documentation": "https://atproto.com",
139
60
})
140
140
-
return nil
141
61
}
142
62
143
63
func (o *OProxy) HandleOAuthPAR(c echo.Context) error {
144
144
-
w.Header().Set("Access-Control-Allow-Origin", "*")
145
145
-
w.Header().Set("Content-Type", "application/json")
146
146
-
var par model.PAR
147
147
-
if err := json.NewDecoder(r.Body).Decode(&par); err != nil {
148
148
-
apierrors.WriteHTTPBadRequest(w, "invalid request", err)
149
149
-
return
64
64
+
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
65
65
+
var par PAR
66
66
+
if err := json.NewDecoder(c.Request().Body).Decode(&par); err != nil {
67
67
+
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
150
68
}
151
69
152
152
-
dpopHeader := r.Header.Get("DPoP")
70
70
+
dpopHeader := c.Request().Header.Get("DPoP")
153
71
if dpopHeader == "" {
154
154
-
apierrors.WriteHTTPBadRequest(w, "DPoP header is required", nil)
155
155
-
return
72
72
+
return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
156
73
}
157
74
158
158
-
thirtySec := time.Duration(30 * time.Second)
159
159
-
proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: r.Host, Scheme: "https", Path: "/api/oauth/par"}, dpop.ParseOptions{
160
160
-
Nonce: "",
161
161
-
TimeWindow: &thirtySec,
162
162
-
})
163
163
-
// Check the error type to determine response
75
75
+
resp, err := o.NewPAR(c.Request().Context(), &par, dpopHeader)
164
76
if err != nil {
165
165
-
if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
166
166
-
apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", nil)
167
167
-
return
168
168
-
}
169
169
-
apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", err)
170
170
-
return
77
77
+
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
171
78
}
172
172
-
173
173
-
// proof is valid, get public key to associate with access token
174
174
-
par.JKT = proof.PublicKey()
175
175
-
176
176
-
if err := a.Model.CreatePAR(&par); err != nil {
177
177
-
apierrors.WriteHTTPInternalServerError(w, "could not create par", err)
178
178
-
return
179
179
-
}
180
180
-
resp := par.ToPARResponse()
181
181
-
w.WriteHeader(201)
182
182
-
json.NewEncoder(w).Encode(resp)
79
79
+
return c.JSON(http.StatusCreated, resp)
183
80
}
184
81
185
82
func (o *OProxy) HandleOAuthAuthorize(c echo.Context) error {
+19
pkg/oproxy/helpers.go
···
1
1
+
package oproxy
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
6
6
+
"github.com/google/uuid"
7
7
+
)
8
8
+
9
9
+
func boolPtr(b bool) *bool {
10
10
+
return &b
11
11
+
}
12
12
+
13
13
+
func codeUUID(prefix string) string {
14
14
+
uu, err := uuid.NewV7()
15
15
+
if err != nil {
16
16
+
panic(err)
17
17
+
}
18
18
+
return fmt.Sprintf("%s-%s", prefix, uu.String())
19
19
+
}
+145
pkg/oproxy/oauth_downstream.go
···
5
5
"crypto/sha256"
6
6
"encoding/base64"
7
7
"fmt"
8
8
+
"net/http"
9
9
+
"net/url"
10
10
+
"slices"
8
11
"time"
9
12
13
13
+
"github.com/AxisCommunications/go-dpop"
10
14
"github.com/golang-jwt/jwt/v5"
11
15
"github.com/google/uuid"
16
16
+
"github.com/labstack/echo/v4"
12
17
"stream.place/streamplace/pkg/config"
13
18
"stream.place/streamplace/pkg/model"
14
19
)
···
180
185
}
181
186
return fmt.Sprintf("code-%s", uu.String()), nil
182
187
}
188
188
+
189
189
+
func generateOAuthServerMetadata(host string) map[string]any {
190
190
+
oauthServerMetadata := map[string]any{
191
191
+
"issuer": fmt.Sprintf("https://%s", host),
192
192
+
"request_parameter_supported": true,
193
193
+
"request_uri_parameter_supported": true,
194
194
+
"require_request_uri_registration": true,
195
195
+
"scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"},
196
196
+
"subject_types_supported": []string{"public"},
197
197
+
"response_types_supported": []string{"code"},
198
198
+
"response_modes_supported": []string{"query", "fragment", "form_post"},
199
199
+
"grant_types_supported": []string{"authorization_code", "refresh_token"},
200
200
+
"code_challenge_methods_supported": []string{"S256"},
201
201
+
"ui_locales_supported": []string{"en-US"},
202
202
+
"display_values_supported": []string{"page", "popup", "touch"},
203
203
+
"authorization_response_iss_parameter_supported": true,
204
204
+
"request_object_encryption_alg_values_supported": []string{},
205
205
+
"request_object_encryption_enc_values_supported": []string{},
206
206
+
"jwks_uri": fmt.Sprintf("https://%s/oauth/jwks", host),
207
207
+
"authorization_endpoint": fmt.Sprintf("https://%s/oauth/authorize", host),
208
208
+
"token_endpoint": fmt.Sprintf("https://%s/oauth/token", host),
209
209
+
"token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"},
210
210
+
"revocation_endpoint": fmt.Sprintf("https://%s/oauth/revoke", host),
211
211
+
"introspection_endpoint": fmt.Sprintf("https://%s/oauth/introspect", host),
212
212
+
"pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/oauth/par", host),
213
213
+
"require_pushed_authorization_requests": true,
214
214
+
"client_id_metadata_document_supported": true,
215
215
+
"request_object_signing_alg_values_supported": []string{
216
216
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
217
217
+
"ES256", "ES256K", "ES384", "ES512", "none",
218
218
+
},
219
219
+
"token_endpoint_auth_signing_alg_values_supported": []string{
220
220
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
221
221
+
"ES256", "ES256K", "ES384", "ES512",
222
222
+
},
223
223
+
"dpop_signing_alg_values_supported": []string{
224
224
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
225
225
+
"ES256", "ES256K", "ES384", "ES512",
226
226
+
},
227
227
+
}
228
228
+
return oauthServerMetadata
229
229
+
}
230
230
+
231
231
+
func (o *OProxy) GetDownstreamMetadata() *OAuthClientMetadata {
232
232
+
meta := &OAuthClientMetadata{
233
233
+
ClientID: fmt.Sprintf("https://%s/oauth/downstream/client-metadata.json", o.host),
234
234
+
ClientURI: fmt.Sprintf("https://%s", o.host),
235
235
+
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
236
236
+
Scope: "atproto transition:generic",
237
237
+
TokenEndpointAuthMethod: "none",
238
238
+
ClientName: "Streamplace",
239
239
+
ResponseTypes: []string{"code"},
240
240
+
GrantTypes: []string{"authorization_code", "refresh_token"},
241
241
+
DPoPBoundAccessTokens: boolPtr(true),
242
242
+
RedirectURIs: []string{fmt.Sprintf("https://%s/login", o.host)},
243
243
+
}
244
244
+
return meta
245
245
+
}
246
246
+
247
247
+
func (o *OProxy) NewPAR(ctx context.Context, par *PAR, dpopHeader string) (*PARResponse, error) {
248
248
+
thirtySec := time.Duration(30 * time.Second)
249
249
+
proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/api/oauth/par"}, dpop.ParseOptions{
250
250
+
Nonce: "",
251
251
+
TimeWindow: &thirtySec,
252
252
+
})
253
253
+
// Check the error type to determine response
254
254
+
if err != nil {
255
255
+
// if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
256
256
+
// apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", nil)
257
257
+
// return
258
258
+
// }
259
259
+
// apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", err)
260
260
+
// return
261
261
+
return nil, err
262
262
+
}
263
263
+
264
264
+
clientMetadata := o.GetDownstreamMetadata()
265
265
+
if par.ClientID != clientMetadata.ClientID {
266
266
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid client_id")
267
267
+
}
268
268
+
269
269
+
if !slices.Contains(clientMetadata.RedirectURIs, par.RedirectURI) {
270
270
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid redirect_uri")
271
271
+
}
272
272
+
273
273
+
if par.CodeChallengeMethod != "S256" {
274
274
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid code challenge method")
275
275
+
}
276
276
+
277
277
+
if par.ResponseMode != "query" {
278
278
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid response mode")
279
279
+
}
280
280
+
281
281
+
if par.ResponseType != "code" {
282
282
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid response type")
283
283
+
}
284
284
+
285
285
+
if par.Scope != o.scope {
286
286
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid scope")
287
287
+
}
288
288
+
289
289
+
if par.LoginHint == "" {
290
290
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "login hint is required to find your PDS")
291
291
+
}
292
292
+
293
293
+
if par.State == "" {
294
294
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "state is required")
295
295
+
}
296
296
+
297
297
+
if par.Scope != o.scope {
298
298
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid scope (expected %s, got %s)", o.scope, par.Scope))
299
299
+
}
300
300
+
301
301
+
// proof is valid, get public key to use as primary key of oauth session
302
302
+
jkt := proof.PublicKey()
303
303
+
uu, err := uuid.NewV7()
304
304
+
if err != nil {
305
305
+
panic(err)
306
306
+
}
307
307
+
308
308
+
urn := fmt.Sprintf("urn:ietf:params:oauth:request_uri:%s", uu.String())
309
309
+
310
310
+
err = o.createOAuthSession(jkt, &OAuthSession{
311
311
+
DownstreamDPoPJKT: jkt,
312
312
+
DownstreamPARRequestURI: urn,
313
313
+
DownstreamCodeChallenge: par.CodeChallenge,
314
314
+
DownstreamState: par.State,
315
315
+
DID: par.LoginHint,
316
316
+
})
317
317
+
if err != nil {
318
318
+
return nil, fmt.Errorf("could not create oauth session: %w", err)
319
319
+
}
320
320
+
321
321
+
resp := &PARResponse{
322
322
+
RequestURI: urn,
323
323
+
ExpiresIn: int(thirtySec.Seconds()),
324
324
+
}
325
325
+
326
326
+
return resp, nil
327
327
+
}
+6
-5
pkg/oproxy/oauth_session.go
···
8
8
9
9
// OAuthSession stores authentication data needed during the OAuth flow
10
10
type OAuthSession struct {
11
11
-
RepoDID string `gorm:"column:repo_did;index"`
12
12
-
PDSUrl string `gorm:"column:pds_url"`
11
11
+
DID string `gorm:"column:repo_did;index"`
13
12
14
13
// Upstream fields
15
14
UpstreamState string `gorm:"column:upstream_state;index"`
···
22
21
UpstreamRefreshToken string `gorm:"column:upstream_refresh_token"`
23
22
24
23
// Downstream fields
25
25
-
DownstreamPARID string `gorm:"column:downstream_par_id;uniqueIndex"`
26
26
-
DownstreamPAR *PAR `gorm:"foreignKey:DownstreamPARID"`
27
24
DownstreamDPoPNonce string `gorm:"column:downstream_dpop_nonce"`
28
28
-
DownstreamDPoPJKT string `gorm:"column:downstream_dpop_jkt"`
25
25
+
DownstreamDPoPJKT string `gorm:"column:downstream_dpop_jkt;primaryKey"`
29
26
DownstreamAccessToken string `gorm:"column:downstream_access_token;index"`
30
27
DownstreamRefreshToken string `gorm:"column:downstream_refresh_token;index"`
31
28
DownstreamAuthorizationCode string `gorm:"column:downstream_authorization_code;index"`
29
29
+
DownstreamState string `gorm:"column:downstream_state"`
30
30
+
DownstreamScope string `gorm:"column:downstream_scope"`
31
31
+
DownstreamCodeChallenge string `gorm:"column:downstream_code_challenge"`
32
32
+
DownstreamPARRequestURI string `gorm:"column:downstream_par_request_uri"`
32
33
33
34
RevokedAt *time.Time `gorm:"column:revoked_at"`
34
35
CreatedAt time.Time
+19
pkg/oproxy/oauth_upstream.go
···
14
14
"github.com/lestrrat-go/jwx/v2/jwk"
15
15
"stream.place/streamplace/pkg/config"
16
16
"stream.place/streamplace/pkg/log"
17
17
+
"stream.place/streamplace/pkg/model"
17
18
)
18
19
19
20
func Login(ctx context.Context, cli *config.CLI, downstreamPAR *model.PAR, mod model.Model) (string, error) {
···
172
173
173
174
return session, nil
174
175
}
176
176
+
177
177
+
func (o *OProxy) GetUpstreamMetadata() *OAuthClientMetadata {
178
178
+
meta := &OAuthClientMetadata{
179
179
+
ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/oauth/upstream/client-metadata.json", o.host),
180
180
+
JwksURI: fmt.Sprintf("https://%s/api/atproto-oauth/jwks.json", o.host),
181
181
+
ClientURI: fmt.Sprintf("https://%s", o.host),
182
182
+
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
183
183
+
Scope: "atproto transition:generic",
184
184
+
TokenEndpointAuthMethod: "private_key_jwt",
185
185
+
ClientName: "Streamplace",
186
186
+
ResponseTypes: []string{"code"},
187
187
+
GrantTypes: []string{"authorization_code", "refresh_token"},
188
188
+
DPoPBoundAccessTokens: boolPtr(true),
189
189
+
TokenEndpointAuthSigningAlg: "ES256",
190
190
+
RedirectURIs: []string{fmt.Sprintf("https://%s/oauth/return", o.host)},
191
191
+
}
192
192
+
return meta
193
193
+
}
+19
-10
pkg/oproxy/oproxy.go
···
3
3
import "github.com/labstack/echo/v4"
4
4
5
5
type OProxy struct {
6
6
-
saveOAuthSession func(id string, session *OAuthSession) error
7
7
-
loadOAuthSession func(id string) (*OAuthSession, error)
8
8
-
e *echo.Echo
6
6
+
createOAuthSession func(id string, session *OAuthSession) error
7
7
+
updateOAuthSession func(id string, session *OAuthSession) error
8
8
+
loadOAuthSession func(id string) (*OAuthSession, error)
9
9
+
e *echo.Echo
10
10
+
host string
11
11
+
scope string
9
12
}
10
13
11
11
-
type OProxyConfig struct {
12
12
-
SaveOAuthSession func(id string, session *OAuthSession) error
13
13
-
LoadOAuthSession func(id string) (*OAuthSession, error)
14
14
+
type Config struct {
15
15
+
CreateOAuthSession func(id string, session *OAuthSession) error
16
16
+
UpdateOAuthSession func(id string, session *OAuthSession) error
17
17
+
LoadOAuthSession func(id string) (*OAuthSession, error)
18
18
+
Host string
19
19
+
Scope string
14
20
}
15
21
16
16
-
func NewOProxy(conf *OProxyConfig) *OProxy {
22
22
+
func New(conf *Config) *OProxy {
17
23
e := echo.New()
18
24
return &OProxy{
19
19
-
saveOAuthSession: conf.SaveOAuthSession,
20
20
-
loadOAuthSession: conf.LoadOAuthSession,
21
21
-
e: e,
25
25
+
createOAuthSession: conf.CreateOAuthSession,
26
26
+
updateOAuthSession: conf.UpdateOAuthSession,
27
27
+
loadOAuthSession: conf.LoadOAuthSession,
28
28
+
e: e,
29
29
+
host: conf.Host,
30
30
+
scope: conf.Scope,
22
31
}
23
32
}
+14
-13
pkg/oproxy/par.go
···
1
1
package oproxy
2
2
3
3
-
import "time"
3
3
+
type PAR struct {
4
4
+
ClientID string `json:"client_id"`
5
5
+
RedirectURI string `json:"redirect_uri"`
6
6
+
CodeChallenge string `json:"code_challenge"`
7
7
+
CodeChallengeMethod string `json:"code_challenge_method"`
8
8
+
State string `json:"state"`
9
9
+
LoginHint string `json:"login_hint"`
10
10
+
ResponseMode string `json:"response_mode"`
11
11
+
ResponseType string `json:"response_type"`
12
12
+
Scope string `json:"scope"`
13
13
+
}
4
14
5
5
-
type PAR struct {
6
6
-
ClientID string `json:"client_id" gorm:"column:client_id;index"`
7
7
-
RedirectURI string `json:"redirect_uri" gorm:"column:redirect_uri"`
8
8
-
CodeChallenge string `json:"code_challenge" gorm:"column:code_challenge;index"`
9
9
-
CodeChallengeMethod string `json:"code_challenge_method" gorm:"column:code_challenge_method"`
10
10
-
State string `json:"state" gorm:"column:state"`
11
11
-
LoginHint string `json:"login_hint" gorm:"column:login_hint"`
12
12
-
ResponseMode string `json:"response_mode" gorm:"column:response_mode"`
13
13
-
ResponseType string `json:"response_type" gorm:"column:response_type"`
14
14
-
Scope string `json:"scope" gorm:"column:scope"`
15
15
-
ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at"`
16
16
-
JKT string `json:"jkt" gorm:"column:jkt"`
15
15
+
type PARResponse struct {
16
16
+
RequestURI string `json:"request_uri"`
17
17
+
ExpiresIn int `json:"expires_in"`
17
18
}