tangled
alpha
login
or
join now
stream.place
/
streamplace
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
oproxy: restructure
Eli Mallon
9 months ago
fb6deb0e
93b4a597
+960
-911
15 changed files
expand all
collapse all
unified
split
js
app
.env.development
pkg
oproxy
handlers.go
oauth_0_metadata.go
oauth_1_par.go
oauth_2_authorize.go
oauth_3_return.go
oauth_4_token.go
oauth_5_revoke.go
oauth_downstream.go
oauth_middleware.go
oauth_upstream.go
oproxy.go
par.go
resolution.go
token_generation.go
+1
-1
js/app/.env.development
···
1
EXPO_PUBLIC_STREAMPLACE_URL=http://127.0.0.1:38080
2
-
EXPO_PUBLIC_WEB_TRY_LOCAL=false
3
EXPO_USE_METRO_WORKSPACE_ROOT=1
···
1
EXPO_PUBLIC_STREAMPLACE_URL=http://127.0.0.1:38080
2
+
EXPO_PUBLIC_WEB_TRY_LOCAL=true
3
EXPO_USE_METRO_WORKSPACE_ROOT=1
-180
pkg/oproxy/handlers.go
···
1
-
package oproxy
2
-
3
-
import (
4
-
"encoding/json"
5
-
"errors"
6
-
"fmt"
7
-
"net/http"
8
-
9
-
"github.com/haileyok/atproto-oauth-golang/helpers"
10
-
"github.com/labstack/echo/v4"
11
-
"go.opentelemetry.io/otel"
12
-
)
13
-
14
-
func (o *OProxy) Handler() http.Handler {
15
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16
-
w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh
17
-
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP")
18
-
w.Header().Set("Access-Control-Allow-Methods", "*")
19
-
w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce")
20
-
o.e.ServeHTTP(w, r)
21
-
})
22
-
}
23
-
24
-
func (o *OProxy) HandleOAuthAuthorizationServer(c echo.Context) error {
25
-
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
26
-
c.Response().Header().Set("Content-Type", "application/json")
27
-
c.Response().WriteHeader(200)
28
-
json.NewEncoder(c.Response().Writer).Encode(generateOAuthServerMetadata(o.host))
29
-
return nil
30
-
}
31
-
32
-
func (o *OProxy) HandleClientMetadataUpstream(c echo.Context) error {
33
-
meta := o.GetUpstreamMetadata()
34
-
return c.JSON(200, meta)
35
-
}
36
-
37
-
func (o *OProxy) HandleJwksUpstream(c echo.Context) error {
38
-
pubKey, err := o.upstreamJWK.PublicKey()
39
-
if err != nil {
40
-
return echo.NewHTTPError(http.StatusInternalServerError, "could not get public key")
41
-
}
42
-
return c.JSON(200, helpers.CreateJwksResponseObject(pubKey))
43
-
}
44
-
45
-
func (o *OProxy) HandleClientMetadataDownstream(c echo.Context) error {
46
-
redirectURI := c.QueryParam("redirect_uri")
47
-
meta, err := o.GetDownstreamMetadata(redirectURI)
48
-
if err != nil {
49
-
return err
50
-
}
51
-
return c.JSON(200, meta)
52
-
}
53
-
54
-
func (o *OProxy) HandleOAuthProtectedResource(c echo.Context) error {
55
-
return c.JSON(200, map[string]interface{}{
56
-
"resource": fmt.Sprintf("https://%s", o.host),
57
-
"authorization_servers": []string{
58
-
fmt.Sprintf("https://%s", o.host),
59
-
},
60
-
"scopes_supported": []string{},
61
-
"bearer_methods_supported": []string{
62
-
"header",
63
-
},
64
-
"resource_documentation": "https://atproto.com",
65
-
})
66
-
}
67
-
68
-
func (o *OProxy) HandleOAuthPAR(c echo.Context) error {
69
-
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthPAR")
70
-
defer span.End()
71
-
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
72
-
var par PAR
73
-
if err := json.NewDecoder(c.Request().Body).Decode(&par); err != nil {
74
-
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
75
-
}
76
-
77
-
dpopHeader := c.Request().Header.Get("DPoP")
78
-
if dpopHeader == "" {
79
-
return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
80
-
}
81
-
82
-
resp, err := o.NewPAR(ctx, c, &par, dpopHeader)
83
-
if errors.Is(err, ErrFirstNonce) {
84
-
res := map[string]interface{}{
85
-
"error": "use_dpop_nonce",
86
-
"error_description": "Authorization server requires nonce in DPoP proof",
87
-
}
88
-
return c.JSON(http.StatusBadRequest, res)
89
-
} else if err != nil {
90
-
return err
91
-
}
92
-
return c.JSON(http.StatusCreated, resp)
93
-
}
94
-
95
-
func (o *OProxy) HandleOAuthAuthorize(c echo.Context) error {
96
-
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthAuthorize")
97
-
defer span.End()
98
-
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
99
-
requestURI := c.QueryParam("request_uri")
100
-
if requestURI == "" {
101
-
return echo.NewHTTPError(http.StatusBadRequest, "request_uri is required")
102
-
}
103
-
clientID := c.QueryParam("client_id")
104
-
if clientID == "" {
105
-
return echo.NewHTTPError(http.StatusBadRequest, "client_id is required")
106
-
}
107
-
redirectURL, err := o.Authorize(ctx, requestURI, clientID)
108
-
if err != nil {
109
-
return err
110
-
}
111
-
return c.Redirect(http.StatusTemporaryRedirect, redirectURL)
112
-
}
113
-
114
-
func (o *OProxy) HandleOAuthReturn(c echo.Context) error {
115
-
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthReturn")
116
-
defer span.End()
117
-
code := c.QueryParam("code")
118
-
iss := c.QueryParam("iss")
119
-
state := c.QueryParam("state")
120
-
redirectURL, err := o.Return(ctx, code, iss, state)
121
-
if err != nil {
122
-
return err
123
-
}
124
-
return c.Redirect(http.StatusTemporaryRedirect, redirectURL)
125
-
}
126
-
127
-
// TokenRequest represents the structure of an OAuth token request
128
-
129
-
func (o *OProxy) HandleOAuthToken(c echo.Context) error {
130
-
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthToken")
131
-
defer span.End()
132
-
var tokenRequest TokenRequest
133
-
if err := json.NewDecoder(c.Request().Body).Decode(&tokenRequest); err != nil {
134
-
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err))
135
-
}
136
-
137
-
dpopHeader := c.Request().Header.Get("DPoP")
138
-
if dpopHeader == "" {
139
-
return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
140
-
}
141
-
142
-
res, err := o.Token(ctx, &tokenRequest, dpopHeader)
143
-
if err != nil {
144
-
return err
145
-
}
146
-
jkt, _, err := getJKT(dpopHeader)
147
-
if err != nil {
148
-
return err
149
-
}
150
-
sess, err := o.loadOAuthSession(jkt)
151
-
if err != nil {
152
-
return err
153
-
}
154
-
sess.DownstreamDPoPNonce = makeNonce()
155
-
err = o.updateOAuthSession(sess.DownstreamDPoPJKT, sess)
156
-
if err != nil {
157
-
return err
158
-
}
159
-
c.Response().Header().Set("DPoP-Nonce", sess.DownstreamDPoPNonce)
160
-
161
-
return c.JSON(http.StatusOK, res)
162
-
}
163
-
164
-
func (o *OProxy) HandleOAuthRevoke(c echo.Context) error {
165
-
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthRevoke")
166
-
defer span.End()
167
-
var revokeRequest RevokeRequest
168
-
if err := json.NewDecoder(c.Request().Body).Decode(&revokeRequest); err != nil {
169
-
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err))
170
-
}
171
-
dpopHeader := c.Request().Header.Get("DPoP")
172
-
if dpopHeader == "" {
173
-
return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
174
-
}
175
-
err := o.Revoke(ctx, dpopHeader, &revokeRequest)
176
-
if err != nil {
177
-
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not handle oauth revoke: %s", err))
178
-
}
179
-
return c.JSON(http.StatusOK, map[string]interface{}{})
180
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+155
pkg/oproxy/oauth_0_metadata.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package oproxy
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
8
+
"github.com/haileyok/atproto-oauth-golang/helpers"
9
+
"github.com/labstack/echo/v4"
10
+
)
11
+
12
+
func (o *OProxy) HandleOAuthAuthorizationServer(c echo.Context) error {
13
+
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
14
+
c.Response().Header().Set("Content-Type", "application/json")
15
+
c.Response().WriteHeader(200)
16
+
json.NewEncoder(c.Response().Writer).Encode(generateOAuthServerMetadata(o.host))
17
+
return nil
18
+
}
19
+
20
+
func (o *OProxy) HandleOAuthProtectedResource(c echo.Context) error {
21
+
return c.JSON(200, map[string]interface{}{
22
+
"resource": fmt.Sprintf("https://%s", o.host),
23
+
"authorization_servers": []string{
24
+
fmt.Sprintf("https://%s", o.host),
25
+
},
26
+
"scopes_supported": []string{},
27
+
"bearer_methods_supported": []string{
28
+
"header",
29
+
},
30
+
"resource_documentation": "https://atproto.com",
31
+
})
32
+
}
33
+
34
+
func (o *OProxy) HandleClientMetadataUpstream(c echo.Context) error {
35
+
meta := o.GetUpstreamMetadata()
36
+
return c.JSON(200, meta)
37
+
}
38
+
39
+
func (o *OProxy) HandleJwksUpstream(c echo.Context) error {
40
+
pubKey, err := o.upstreamJWK.PublicKey()
41
+
if err != nil {
42
+
return echo.NewHTTPError(http.StatusInternalServerError, "could not get public key")
43
+
}
44
+
return c.JSON(200, helpers.CreateJwksResponseObject(pubKey))
45
+
}
46
+
47
+
func (o *OProxy) HandleClientMetadataDownstream(c echo.Context) error {
48
+
redirectURI := c.QueryParam("redirect_uri")
49
+
meta, err := o.GetDownstreamMetadata(redirectURI)
50
+
if err != nil {
51
+
return err
52
+
}
53
+
return c.JSON(200, meta)
54
+
}
55
+
56
+
func (o *OProxy) GetUpstreamMetadata() *OAuthClientMetadata {
57
+
// publicKey, err := o.upstreamJWK.PublicKey()
58
+
// if err != nil {
59
+
// panic(err)
60
+
// }
61
+
// jwks := jwk.NewSet()
62
+
// err = jwks.AddKey(publicKey)
63
+
// if err != nil {
64
+
// panic(err)
65
+
// }
66
+
// ro := helpers.CreateJwksResponseObject(publicKey)
67
+
meta := &OAuthClientMetadata{
68
+
ClientID: fmt.Sprintf("https://%s/oauth/upstream/client-metadata.json", o.host),
69
+
JwksURI: fmt.Sprintf("https://%s/oauth/upstream/jwks.json", o.host),
70
+
ClientURI: fmt.Sprintf("https://%s", o.host),
71
+
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
72
+
Scope: "atproto transition:generic",
73
+
TokenEndpointAuthMethod: "private_key_jwt",
74
+
ClientName: "Streamplace",
75
+
ResponseTypes: []string{"code"},
76
+
GrantTypes: []string{"authorization_code", "refresh_token"},
77
+
DPoPBoundAccessTokens: boolPtr(true),
78
+
TokenEndpointAuthSigningAlg: "ES256",
79
+
RedirectURIs: []string{fmt.Sprintf("https://%s/oauth/return", o.host)},
80
+
// Jwks: ro,
81
+
}
82
+
return meta
83
+
}
84
+
85
+
func generateOAuthServerMetadata(host string) map[string]any {
86
+
oauthServerMetadata := map[string]any{
87
+
"issuer": fmt.Sprintf("https://%s", host),
88
+
"request_parameter_supported": true,
89
+
"request_uri_parameter_supported": true,
90
+
"require_request_uri_registration": true,
91
+
"scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"},
92
+
"subject_types_supported": []string{"public"},
93
+
"response_types_supported": []string{"code"},
94
+
"response_modes_supported": []string{"query", "fragment", "form_post"},
95
+
"grant_types_supported": []string{"authorization_code", "refresh_token"},
96
+
"code_challenge_methods_supported": []string{"S256"},
97
+
"ui_locales_supported": []string{"en-US"},
98
+
"display_values_supported": []string{"page", "popup", "touch"},
99
+
"authorization_response_iss_parameter_supported": true,
100
+
"request_object_encryption_alg_values_supported": []string{},
101
+
"request_object_encryption_enc_values_supported": []string{},
102
+
"jwks_uri": fmt.Sprintf("https://%s/oauth/jwks", host),
103
+
"authorization_endpoint": fmt.Sprintf("https://%s/oauth/authorize", host),
104
+
"token_endpoint": fmt.Sprintf("https://%s/oauth/token", host),
105
+
"token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"},
106
+
"revocation_endpoint": fmt.Sprintf("https://%s/oauth/revoke", host),
107
+
"introspection_endpoint": fmt.Sprintf("https://%s/oauth/introspect", host),
108
+
"pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/oauth/par", host),
109
+
"require_pushed_authorization_requests": true,
110
+
"client_id_metadata_document_supported": true,
111
+
"request_object_signing_alg_values_supported": []string{
112
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
113
+
"ES256", "ES256K", "ES384", "ES512", "none",
114
+
},
115
+
"token_endpoint_auth_signing_alg_values_supported": []string{
116
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
117
+
"ES256", "ES256K", "ES384", "ES512",
118
+
},
119
+
"dpop_signing_alg_values_supported": []string{
120
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
121
+
"ES256", "ES256K", "ES384", "ES512",
122
+
},
123
+
}
124
+
return oauthServerMetadata
125
+
}
126
+
127
+
func (o *OProxy) GetDownstreamMetadata(redirectURI string) (*OAuthClientMetadata, error) {
128
+
meta := &OAuthClientMetadata{
129
+
ClientID: fmt.Sprintf("https://%s/oauth/downstream/client-metadata.json", o.host),
130
+
ClientURI: fmt.Sprintf("https://%s", o.host),
131
+
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
132
+
Scope: "atproto transition:generic",
133
+
TokenEndpointAuthMethod: "none",
134
+
ClientName: "Streamplace",
135
+
ResponseTypes: []string{"code"},
136
+
GrantTypes: []string{"authorization_code", "refresh_token"},
137
+
DPoPBoundAccessTokens: boolPtr(true),
138
+
RedirectURIs: []string{fmt.Sprintf("https://%s/login", o.host), fmt.Sprintf("https://%s/api/app-return", o.host)},
139
+
ApplicationType: "web",
140
+
}
141
+
if redirectURI != "" {
142
+
found := false
143
+
for _, uri := range meta.RedirectURIs {
144
+
if uri == redirectURI {
145
+
found = true
146
+
break
147
+
}
148
+
}
149
+
if !found {
150
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", redirectURI))
151
+
}
152
+
meta.RedirectURIs = []string{redirectURI}
153
+
}
154
+
return meta, nil
155
+
}
+178
pkg/oproxy/oauth_1_par.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package oproxy
2
+
3
+
import (
4
+
"context"
5
+
"encoding/json"
6
+
"errors"
7
+
"fmt"
8
+
"net/http"
9
+
"net/url"
10
+
"slices"
11
+
12
+
"github.com/AxisCommunications/go-dpop"
13
+
"github.com/labstack/echo/v4"
14
+
"go.opentelemetry.io/otel"
15
+
)
16
+
17
+
type PAR struct {
18
+
ClientID string `json:"client_id"`
19
+
RedirectURI string `json:"redirect_uri"`
20
+
CodeChallenge string `json:"code_challenge"`
21
+
CodeChallengeMethod string `json:"code_challenge_method"`
22
+
State string `json:"state"`
23
+
LoginHint string `json:"login_hint"`
24
+
ResponseMode string `json:"response_mode"`
25
+
ResponseType string `json:"response_type"`
26
+
Scope string `json:"scope"`
27
+
}
28
+
29
+
type PARResponse struct {
30
+
RequestURI string `json:"request_uri"`
31
+
ExpiresIn int `json:"expires_in"`
32
+
}
33
+
34
+
var ErrFirstNonce = echo.NewHTTPError(http.StatusBadRequest, "first time seeing this key, come back with a nonce")
35
+
36
+
func (o *OProxy) HandleOAuthPAR(c echo.Context) error {
37
+
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthPAR")
38
+
defer span.End()
39
+
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
40
+
var par PAR
41
+
if err := json.NewDecoder(c.Request().Body).Decode(&par); err != nil {
42
+
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
43
+
}
44
+
45
+
dpopHeader := c.Request().Header.Get("DPoP")
46
+
if dpopHeader == "" {
47
+
return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
48
+
}
49
+
50
+
resp, err := o.NewPAR(ctx, c, &par, dpopHeader)
51
+
if errors.Is(err, ErrFirstNonce) {
52
+
res := map[string]interface{}{
53
+
"error": "use_dpop_nonce",
54
+
"error_description": "Authorization server requires nonce in DPoP proof",
55
+
}
56
+
return c.JSON(http.StatusBadRequest, res)
57
+
} else if err != nil {
58
+
return err
59
+
}
60
+
return c.JSON(http.StatusCreated, resp)
61
+
}
62
+
63
+
func (o *OProxy) NewPAR(ctx context.Context, c echo.Context, par *PAR, dpopHeader string) (*PARResponse, error) {
64
+
jkt, nonce, err := getJKT(dpopHeader)
65
+
if err != nil {
66
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get JKT from DPoP header header=%s: %s", dpopHeader, err))
67
+
}
68
+
session, err := o.loadOAuthSession(jkt)
69
+
if err != nil {
70
+
return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to load OAuth session: %s", err))
71
+
}
72
+
// special case - if this is the first request, we need to send it back for a new nonce
73
+
if session == nil {
74
+
_, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{
75
+
Nonce: nonce, // normally this would be bad! but on the first request we're revalidating nonce anyway
76
+
TimeWindow: &dpopTimeWindow,
77
+
})
78
+
if err != nil {
79
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse DPoP header: %s", err))
80
+
}
81
+
newNonce := makeNonce()
82
+
err = o.createOAuthSession(jkt, &OAuthSession{
83
+
DownstreamDPoPJKT: jkt,
84
+
DownstreamDPoPNonce: newNonce,
85
+
})
86
+
if err != nil {
87
+
return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth session: %s", err))
88
+
}
89
+
// come back later, nerd
90
+
c.Response().Header().Set("DPoP-Nonce", newNonce)
91
+
return nil, ErrFirstNonce
92
+
}
93
+
if session.DownstreamDPoPNonce != nonce {
94
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid nonce")
95
+
}
96
+
proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{
97
+
Nonce: session.DownstreamDPoPNonce,
98
+
TimeWindow: &dpopTimeWindow,
99
+
})
100
+
// Check the error type to determine response
101
+
if err != nil {
102
+
// if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
103
+
// apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", nil)
104
+
// return
105
+
// }
106
+
// apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", err)
107
+
// return
108
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid DPoP proof: %s", err))
109
+
}
110
+
if proof.PublicKey() != jkt {
111
+
panic("invalid code path: parsed DPoP proof twice and got different keys?!")
112
+
}
113
+
114
+
clientMetadata, err := o.GetDownstreamMetadata(par.RedirectURI)
115
+
if err != nil {
116
+
return nil, err
117
+
}
118
+
if par.ClientID != clientMetadata.ClientID {
119
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid client_id: expected %s, got %s", clientMetadata.ClientID, par.ClientID))
120
+
}
121
+
122
+
if !slices.Contains(clientMetadata.RedirectURIs, par.RedirectURI) {
123
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", par.RedirectURI))
124
+
}
125
+
126
+
if par.CodeChallengeMethod != "S256" {
127
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid code challenge method: expected S256, got %s", par.CodeChallengeMethod))
128
+
}
129
+
130
+
if par.ResponseMode != "query" {
131
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response mode: expected query, got %s", par.ResponseMode))
132
+
}
133
+
134
+
if par.ResponseType != "code" {
135
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response type: expected code, got %s", par.ResponseType))
136
+
}
137
+
138
+
if par.Scope != o.scope {
139
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid scope")
140
+
}
141
+
142
+
if par.LoginHint == "" {
143
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "login hint is required to find your PDS")
144
+
}
145
+
146
+
if par.State == "" {
147
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "state is required")
148
+
}
149
+
150
+
if par.Scope != o.scope {
151
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid scope (expected %s, got %s)", o.scope, par.Scope))
152
+
}
153
+
154
+
urn := makeURN(jkt)
155
+
156
+
newNonce := makeNonce()
157
+
158
+
err = o.updateOAuthSession(jkt, &OAuthSession{
159
+
DownstreamDPoPJKT: jkt,
160
+
DownstreamDPoPNonce: newNonce,
161
+
DownstreamPARRequestURI: urn,
162
+
DownstreamCodeChallenge: par.CodeChallenge,
163
+
DownstreamState: par.State,
164
+
DownstreamRedirectURI: par.RedirectURI,
165
+
DID: par.LoginHint,
166
+
})
167
+
if err != nil {
168
+
return nil, fmt.Errorf("could not create oauth session: %w", err)
169
+
}
170
+
c.Response().Header().Set("DPoP-Nonce", newNonce)
171
+
172
+
resp := &PARResponse{
173
+
RequestURI: urn,
174
+
ExpiresIn: int(dpopTimeWindow.Seconds()),
175
+
}
176
+
177
+
return resp, nil
178
+
}
+146
pkg/oproxy/oauth_2_authorize.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package oproxy
2
+
3
+
import (
4
+
"context"
5
+
"encoding/json"
6
+
"fmt"
7
+
"net/http"
8
+
"net/url"
9
+
"time"
10
+
11
+
oauth "github.com/haileyok/atproto-oauth-golang"
12
+
"github.com/haileyok/atproto-oauth-golang/helpers"
13
+
"github.com/labstack/echo/v4"
14
+
"go.opentelemetry.io/otel"
15
+
)
16
+
17
+
func (o *OProxy) HandleOAuthAuthorize(c echo.Context) error {
18
+
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthAuthorize")
19
+
defer span.End()
20
+
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
21
+
requestURI := c.QueryParam("request_uri")
22
+
if requestURI == "" {
23
+
return echo.NewHTTPError(http.StatusBadRequest, "request_uri is required")
24
+
}
25
+
clientID := c.QueryParam("client_id")
26
+
if clientID == "" {
27
+
return echo.NewHTTPError(http.StatusBadRequest, "client_id is required")
28
+
}
29
+
redirectURL, err := o.Authorize(ctx, requestURI, clientID)
30
+
if err != nil {
31
+
return err
32
+
}
33
+
return c.Redirect(http.StatusTemporaryRedirect, redirectURL)
34
+
}
35
+
36
+
// downstream --> upstream transition; attempt to send user to the upstream auth server
37
+
func (o *OProxy) Authorize(ctx context.Context, requestURI, clientID string) (string, error) {
38
+
downstreamMeta, err := o.GetDownstreamMetadata("")
39
+
if err != nil {
40
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get downstream metadata: %s", err))
41
+
}
42
+
if downstreamMeta.ClientID != clientID {
43
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("client ID mismatch: %s != %s", downstreamMeta.ClientID, clientID))
44
+
}
45
+
46
+
jkt, _, err := parseURN(requestURI)
47
+
if err != nil {
48
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err))
49
+
}
50
+
51
+
session, err := o.loadOAuthSession(jkt)
52
+
if err != nil {
53
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err))
54
+
}
55
+
56
+
if session == nil {
57
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no session found for jkt=%s", jkt))
58
+
}
59
+
60
+
if session.Status() != OAuthSessionStatePARCreated {
61
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in par-created state: %s", session.Status()))
62
+
}
63
+
64
+
if session.DownstreamPARRequestURI != requestURI {
65
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("request URI mismatch: %s != %s", session.DownstreamPARRequestURI, requestURI))
66
+
}
67
+
68
+
now := time.Now()
69
+
session.DownstreamPARUsedAt = &now
70
+
err = o.updateOAuthSession(jkt, session)
71
+
if err != nil {
72
+
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
73
+
}
74
+
75
+
upstreamMeta := o.GetUpstreamMetadata()
76
+
oclient, err := oauth.NewClient(oauth.ClientArgs{
77
+
ClientJwk: o.upstreamJWK,
78
+
ClientId: upstreamMeta.ClientID,
79
+
RedirectUri: upstreamMeta.RedirectURIs[0],
80
+
})
81
+
if err != nil {
82
+
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth client: %s", err))
83
+
}
84
+
85
+
did, err := resolveHandle(ctx, session.DID)
86
+
if err != nil {
87
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve handle '%s': %s", session.DID, err))
88
+
}
89
+
90
+
service, err := resolveService(ctx, did)
91
+
if err != nil {
92
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve service for DID '%s': %s", did, err))
93
+
}
94
+
95
+
authserver, err := oclient.ResolvePdsAuthServer(ctx, service)
96
+
if err != nil {
97
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve PDS auth server for service '%s': %s", service, err))
98
+
}
99
+
100
+
authmeta, err := oclient.FetchAuthServerMetadata(ctx, authserver)
101
+
if err != nil {
102
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to fetch auth server metadata from '%s': %s", authserver, err))
103
+
}
104
+
105
+
k, err := helpers.GenerateKey(nil)
106
+
if err != nil {
107
+
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate DPoP key: %s", err))
108
+
}
109
+
110
+
state := makeState(jkt)
111
+
112
+
opts := oauth.ParAuthRequestOpts{
113
+
State: state,
114
+
}
115
+
parResp, err := oclient.SendParAuthRequest(ctx, authserver, authmeta, did, upstreamMeta.Scope, k, opts)
116
+
if err != nil {
117
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to send PAR auth request to '%s': %s", authserver, err))
118
+
}
119
+
120
+
jwkJSON, err := json.Marshal(k)
121
+
if err != nil {
122
+
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to marshal DPoP key to JSON: %s", err))
123
+
}
124
+
125
+
u, err := url.Parse(authmeta.AuthorizationEndpoint)
126
+
if err != nil {
127
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse auth server metadata: %s", err))
128
+
}
129
+
u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(upstreamMeta.ClientID), parResp.RequestUri)
130
+
str := u.String()
131
+
132
+
session.DID = did
133
+
session.PDSUrl = service
134
+
session.UpstreamState = parResp.State
135
+
session.UpstreamAuthServerIssuer = authserver
136
+
session.UpstreamPKCEVerifier = parResp.PkceVerifier
137
+
session.UpstreamDPoPNonce = parResp.DpopAuthserverNonce
138
+
session.UpstreamDPoPPrivateJWK = string(jwkJSON)
139
+
140
+
err = o.updateOAuthSession(jkt, session)
141
+
if err != nil {
142
+
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
143
+
}
144
+
145
+
return str, nil
146
+
}
+129
pkg/oproxy/oauth_3_return.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package oproxy
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"net/http"
7
+
"net/url"
8
+
"time"
9
+
10
+
"github.com/bluesky-social/indigo/api/atproto"
11
+
"github.com/bluesky-social/indigo/xrpc"
12
+
oauth "github.com/haileyok/atproto-oauth-golang"
13
+
"github.com/labstack/echo/v4"
14
+
"github.com/lestrrat-go/jwx/v2/jwk"
15
+
"go.opentelemetry.io/otel"
16
+
)
17
+
18
+
func (o *OProxy) HandleOAuthReturn(c echo.Context) error {
19
+
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthReturn")
20
+
defer span.End()
21
+
code := c.QueryParam("code")
22
+
iss := c.QueryParam("iss")
23
+
state := c.QueryParam("state")
24
+
redirectURL, err := o.Return(ctx, code, iss, state)
25
+
if err != nil {
26
+
return err
27
+
}
28
+
return c.Redirect(http.StatusTemporaryRedirect, redirectURL)
29
+
}
30
+
31
+
func (o *OProxy) Return(ctx context.Context, code string, iss string, state string) (string, error) {
32
+
upstreamMeta := o.GetUpstreamMetadata()
33
+
oclient, err := oauth.NewClient(oauth.ClientArgs{
34
+
ClientJwk: o.upstreamJWK,
35
+
ClientId: upstreamMeta.ClientID,
36
+
RedirectUri: upstreamMeta.RedirectURIs[0],
37
+
})
38
+
39
+
jkt, _, err := parseState(state)
40
+
if err != nil {
41
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse state: %s", err))
42
+
}
43
+
44
+
session, err := o.loadOAuthSession(jkt)
45
+
if err != nil {
46
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get OAuth session: %s", err))
47
+
}
48
+
if session == nil {
49
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no OAuth session found for state: %s", state))
50
+
}
51
+
52
+
if session.Status() != OAuthSessionStateUpstream {
53
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in upstream state: %s", session.Status()))
54
+
}
55
+
56
+
if session.UpstreamState != state {
57
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("state mismatch: %s != %s", session.UpstreamState, state))
58
+
}
59
+
60
+
if iss != session.UpstreamAuthServerIssuer {
61
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("issuer mismatch: %s != %s", iss, session.UpstreamAuthServerIssuer))
62
+
}
63
+
64
+
key, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK))
65
+
if err != nil {
66
+
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to parse DPoP private JWK: %s", err))
67
+
}
68
+
69
+
itResp, err := oclient.InitialTokenRequest(ctx, code, iss, session.UpstreamPKCEVerifier, session.UpstreamDPoPNonce, key)
70
+
if err != nil {
71
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to request initial token: %s", err))
72
+
}
73
+
now := time.Now()
74
+
75
+
if itResp.Sub != session.DID {
76
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("sub mismatch: %s != %s", itResp.Sub, session.DID))
77
+
}
78
+
79
+
if itResp.Scope != upstreamMeta.Scope {
80
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("scope mismatch: %s != %s", itResp.Scope, upstreamMeta.Scope))
81
+
}
82
+
83
+
downstreamCode, err := generateAuthorizationCode()
84
+
if err != nil {
85
+
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate downstream code: %s", err))
86
+
}
87
+
88
+
expiry := now.Add(time.Second * time.Duration(itResp.ExpiresIn)).UTC()
89
+
session.UpstreamAccessToken = itResp.AccessToken
90
+
session.UpstreamAccessTokenExp = &expiry
91
+
session.UpstreamRefreshToken = itResp.RefreshToken
92
+
session.DownstreamAuthorizationCode = downstreamCode
93
+
94
+
authArgs := &oauth.XrpcAuthedRequestArgs{
95
+
Did: session.DID,
96
+
AccessToken: session.UpstreamAccessToken,
97
+
PdsUrl: session.PDSUrl,
98
+
Issuer: session.UpstreamAuthServerIssuer,
99
+
DpopPdsNonce: session.UpstreamDPoPNonce,
100
+
DpopPrivateJwk: key,
101
+
}
102
+
103
+
xrpcClient := &oauth.XrpcClient{
104
+
OnDpopPdsNonceChanged: func(did, newNonce string) {},
105
+
}
106
+
107
+
// brief check to make sure we can actually do stuff
108
+
var out atproto.ServerCheckAccountStatus_Output
109
+
if err := xrpcClient.Do(ctx, authArgs, xrpc.Query, "application/json", "com.atproto.server.checkAccountStatus", nil, nil, &out); err != nil {
110
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to check account status: %s", err))
111
+
}
112
+
113
+
err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
114
+
if err != nil {
115
+
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
116
+
}
117
+
118
+
u, err := url.Parse(session.DownstreamRedirectURI)
119
+
if err != nil {
120
+
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err))
121
+
}
122
+
q := u.Query()
123
+
q.Set("iss", fmt.Sprintf("https://%s", o.host))
124
+
q.Set("state", session.DownstreamState)
125
+
q.Set("code", session.DownstreamAuthorizationCode)
126
+
u.RawQuery = q.Encode()
127
+
128
+
return u.String(), nil
129
+
}
+221
pkg/oproxy/oauth_4_token.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package oproxy
2
+
3
+
import (
4
+
"context"
5
+
"crypto/sha256"
6
+
"encoding/base64"
7
+
"encoding/json"
8
+
"fmt"
9
+
"net/http"
10
+
"net/url"
11
+
"time"
12
+
13
+
"github.com/AxisCommunications/go-dpop"
14
+
"github.com/golang-jwt/jwt/v5"
15
+
"github.com/google/uuid"
16
+
"github.com/labstack/echo/v4"
17
+
"go.opentelemetry.io/otel"
18
+
)
19
+
20
+
type TokenRequest struct {
21
+
GrantType string `json:"grant_type"`
22
+
RedirectURI string `json:"redirect_uri"`
23
+
Code string `json:"code"`
24
+
CodeVerifier string `json:"code_verifier"`
25
+
ClientID string `json:"client_id"`
26
+
RefreshToken string `json:"refresh_token"`
27
+
}
28
+
29
+
type RevokeRequest struct {
30
+
Token string `json:"token"`
31
+
ClientID string `json:"client_id"`
32
+
}
33
+
34
+
type TokenResponse struct {
35
+
AccessToken string `json:"access_token"`
36
+
TokenType string `json:"token_type"`
37
+
RefreshToken string `json:"refresh_token"`
38
+
Scope string `json:"scope"`
39
+
ExpiresIn int `json:"expires_in"`
40
+
Sub string `json:"sub"`
41
+
}
42
+
43
+
var OAuthTokenExpiry = time.Hour * 24
44
+
45
+
var dpopTimeWindow = time.Duration(30 * time.Second)
46
+
47
+
func (o *OProxy) HandleOAuthToken(c echo.Context) error {
48
+
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthToken")
49
+
defer span.End()
50
+
var tokenRequest TokenRequest
51
+
if err := json.NewDecoder(c.Request().Body).Decode(&tokenRequest); err != nil {
52
+
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err))
53
+
}
54
+
55
+
dpopHeader := c.Request().Header.Get("DPoP")
56
+
if dpopHeader == "" {
57
+
return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
58
+
}
59
+
60
+
res, err := o.Token(ctx, &tokenRequest, dpopHeader)
61
+
if err != nil {
62
+
return err
63
+
}
64
+
jkt, _, err := getJKT(dpopHeader)
65
+
if err != nil {
66
+
return err
67
+
}
68
+
sess, err := o.loadOAuthSession(jkt)
69
+
if err != nil {
70
+
return err
71
+
}
72
+
sess.DownstreamDPoPNonce = makeNonce()
73
+
err = o.updateOAuthSession(sess.DownstreamDPoPJKT, sess)
74
+
if err != nil {
75
+
return err
76
+
}
77
+
c.Response().Header().Set("DPoP-Nonce", sess.DownstreamDPoPNonce)
78
+
79
+
return c.JSON(http.StatusOK, res)
80
+
}
81
+
82
+
func (o *OProxy) Token(ctx context.Context, tokenRequest *TokenRequest, dpopHeader string) (*TokenResponse, error) {
83
+
proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/token"}, dpop.ParseOptions{
84
+
Nonce: "",
85
+
TimeWindow: &dpopTimeWindow,
86
+
})
87
+
if err != nil {
88
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof")
89
+
}
90
+
91
+
jkt := proof.PublicKey()
92
+
session, err := o.loadOAuthSession(jkt)
93
+
if err != nil {
94
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not get oauth session: %s", err))
95
+
}
96
+
97
+
if tokenRequest.GrantType == "authorization_code" {
98
+
return o.AccessToken(ctx, tokenRequest, session)
99
+
} else if tokenRequest.GrantType == "refresh_token" {
100
+
return o.RefreshToken(ctx, tokenRequest, session)
101
+
}
102
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "unsupported grant type")
103
+
}
104
+
105
+
func (o *OProxy) AccessToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) {
106
+
if session.Status() != OAuthSessionStateDownstream {
107
+
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in downstream state: %s", session.Status()))
108
+
}
109
+
110
+
// Hash the code verifier using SHA-256
111
+
hasher := sha256.New()
112
+
hasher.Write([]byte(tokenRequest.CodeVerifier))
113
+
codeChallenge := hasher.Sum(nil)
114
+
115
+
encodedChallenge := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(codeChallenge)
116
+
117
+
if session.DownstreamCodeChallenge != encodedChallenge {
118
+
return nil, fmt.Errorf("invalid code challenge")
119
+
}
120
+
121
+
if session.DownstreamAuthorizationCode != tokenRequest.Code {
122
+
return nil, fmt.Errorf("invalid authorization code")
123
+
}
124
+
125
+
accessToken, err := o.generateJWT(session)
126
+
if err != nil {
127
+
return nil, fmt.Errorf("could not generate access token: %w", err)
128
+
}
129
+
130
+
refreshToken, err := generateRefreshToken()
131
+
if err != nil {
132
+
return nil, fmt.Errorf("could not generate refresh token: %w", err)
133
+
}
134
+
135
+
session.DownstreamAccessToken = accessToken
136
+
session.DownstreamRefreshToken = refreshToken
137
+
138
+
err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
139
+
if err != nil {
140
+
return nil, fmt.Errorf("could not update downstream session: %w", err)
141
+
}
142
+
143
+
return &TokenResponse{
144
+
AccessToken: accessToken,
145
+
TokenType: "DPoP",
146
+
RefreshToken: refreshToken,
147
+
Scope: "atproto transition:generic",
148
+
ExpiresIn: int(OAuthTokenExpiry.Seconds()),
149
+
Sub: session.DID,
150
+
}, nil
151
+
}
152
+
153
+
func (o *OProxy) RefreshToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) {
154
+
155
+
if session.Status() != OAuthSessionStateReady {
156
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "session is not in ready state")
157
+
}
158
+
159
+
if session.DownstreamRefreshToken != tokenRequest.RefreshToken {
160
+
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid refresh token")
161
+
}
162
+
163
+
newJWT, err := o.generateJWT(session)
164
+
if err != nil {
165
+
return nil, fmt.Errorf("could not generate new access token: %w", err)
166
+
}
167
+
168
+
session.DownstreamAccessToken = newJWT
169
+
err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
170
+
if err != nil {
171
+
return nil, fmt.Errorf("could not update downstream session: %w", err)
172
+
}
173
+
174
+
return &TokenResponse{
175
+
AccessToken: newJWT,
176
+
TokenType: "DPoP",
177
+
RefreshToken: session.DownstreamRefreshToken,
178
+
Scope: "atproto transition:generic",
179
+
ExpiresIn: int(OAuthTokenExpiry.Seconds()),
180
+
Sub: session.DID,
181
+
}, nil
182
+
}
183
+
184
+
func (o *OProxy) generateJWT(session *OAuthSession) (string, error) {
185
+
uu, err := uuid.NewV7()
186
+
if err != nil {
187
+
return "", err
188
+
}
189
+
downstreamMeta, err := o.GetDownstreamMetadata("")
190
+
if err != nil {
191
+
return "", err
192
+
}
193
+
now := time.Now()
194
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
195
+
"jti": uu.String(),
196
+
"sub": session.DID,
197
+
"exp": now.Add(OAuthTokenExpiry).Unix(),
198
+
"iat": now.Unix(),
199
+
"nbf": now.Unix(),
200
+
"cnf": map[string]any{
201
+
"jkt": session.DownstreamDPoPJKT,
202
+
},
203
+
"aud": fmt.Sprintf("did:web:%s", o.host),
204
+
"scope": downstreamMeta.Scope,
205
+
"client_id": downstreamMeta.ClientID,
206
+
"iss": fmt.Sprintf("https://%s", o.host),
207
+
})
208
+
209
+
var rawKey any
210
+
if err := o.downstreamJWK.Raw(&rawKey); err != nil {
211
+
return "", err
212
+
}
213
+
214
+
tokenString, err := token.SignedString(rawKey)
215
+
216
+
if err != nil {
217
+
return "", err
218
+
}
219
+
220
+
return tokenString, nil
221
+
}
+56
pkg/oproxy/oauth_5_revoke.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package oproxy
2
+
3
+
import (
4
+
"context"
5
+
"encoding/json"
6
+
"fmt"
7
+
"net/http"
8
+
"net/url"
9
+
"time"
10
+
11
+
"github.com/AxisCommunications/go-dpop"
12
+
"github.com/labstack/echo/v4"
13
+
"go.opentelemetry.io/otel"
14
+
)
15
+
16
+
func (o *OProxy) HandleOAuthRevoke(c echo.Context) error {
17
+
ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleOAuthRevoke")
18
+
defer span.End()
19
+
var revokeRequest RevokeRequest
20
+
if err := json.NewDecoder(c.Request().Body).Decode(&revokeRequest); err != nil {
21
+
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid request: %s", err))
22
+
}
23
+
dpopHeader := c.Request().Header.Get("DPoP")
24
+
if dpopHeader == "" {
25
+
return echo.NewHTTPError(http.StatusUnauthorized, "DPoP header is required")
26
+
}
27
+
err := o.Revoke(ctx, dpopHeader, &revokeRequest)
28
+
if err != nil {
29
+
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not handle oauth revoke: %s", err))
30
+
}
31
+
return c.JSON(http.StatusOK, map[string]interface{}{})
32
+
}
33
+
34
+
func (o *OProxy) Revoke(ctx context.Context, dpopHeader string, revokeRequest *RevokeRequest) error {
35
+
proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/revoke"}, dpop.ParseOptions{
36
+
Nonce: "",
37
+
TimeWindow: &dpopTimeWindow,
38
+
})
39
+
if err != nil {
40
+
return echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof")
41
+
}
42
+
43
+
session, err := o.loadOAuthSession(proof.PublicKey())
44
+
if err != nil {
45
+
return fmt.Errorf("could not get downstream session: %w", err)
46
+
}
47
+
48
+
now := time.Now()
49
+
session.RevokedAt = &now
50
+
err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
51
+
if err != nil {
52
+
return fmt.Errorf("could not update downstream session: %w", err)
53
+
}
54
+
55
+
return nil
56
+
}
-454
pkg/oproxy/oauth_downstream.go
···
1
-
package oproxy
2
-
3
-
import (
4
-
"context"
5
-
"crypto/sha256"
6
-
"encoding/base64"
7
-
"fmt"
8
-
"net/http"
9
-
"net/url"
10
-
"slices"
11
-
"time"
12
-
13
-
"github.com/AxisCommunications/go-dpop"
14
-
"github.com/golang-jwt/jwt/v5"
15
-
"github.com/google/uuid"
16
-
"github.com/labstack/echo/v4"
17
-
)
18
-
19
-
type TokenRequest struct {
20
-
GrantType string `json:"grant_type"`
21
-
RedirectURI string `json:"redirect_uri"`
22
-
Code string `json:"code"`
23
-
CodeVerifier string `json:"code_verifier"`
24
-
ClientID string `json:"client_id"`
25
-
RefreshToken string `json:"refresh_token"`
26
-
}
27
-
28
-
type RevokeRequest struct {
29
-
Token string `json:"token"`
30
-
ClientID string `json:"client_id"`
31
-
}
32
-
33
-
type TokenResponse struct {
34
-
AccessToken string `json:"access_token"`
35
-
TokenType string `json:"token_type"`
36
-
RefreshToken string `json:"refresh_token"`
37
-
Scope string `json:"scope"`
38
-
ExpiresIn int `json:"expires_in"`
39
-
Sub string `json:"sub"`
40
-
}
41
-
42
-
var OAuthTokenExpiry = time.Hour * 24
43
-
44
-
var dpopTimeWindow = time.Duration(30 * time.Second)
45
-
46
-
func (o *OProxy) Token(ctx context.Context, tokenRequest *TokenRequest, dpopHeader string) (*TokenResponse, error) {
47
-
proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/token"}, dpop.ParseOptions{
48
-
Nonce: "",
49
-
TimeWindow: &dpopTimeWindow,
50
-
})
51
-
if err != nil {
52
-
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof")
53
-
}
54
-
55
-
jkt := proof.PublicKey()
56
-
session, err := o.loadOAuthSession(jkt)
57
-
if err != nil {
58
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not get oauth session: %s", err))
59
-
}
60
-
61
-
if tokenRequest.GrantType == "authorization_code" {
62
-
return o.AccessToken(ctx, tokenRequest, session)
63
-
} else if tokenRequest.GrantType == "refresh_token" {
64
-
return o.RefreshToken(ctx, tokenRequest, session)
65
-
}
66
-
return nil, echo.NewHTTPError(http.StatusBadRequest, "unsupported grant type")
67
-
}
68
-
69
-
func (o *OProxy) AccessToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) {
70
-
if session.Status() != OAuthSessionStateDownstream {
71
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in downstream state: %s", session.Status()))
72
-
}
73
-
74
-
// Hash the code verifier using SHA-256
75
-
hasher := sha256.New()
76
-
hasher.Write([]byte(tokenRequest.CodeVerifier))
77
-
codeChallenge := hasher.Sum(nil)
78
-
79
-
encodedChallenge := base64.RawURLEncoding.WithPadding(base64.NoPadding).EncodeToString(codeChallenge)
80
-
81
-
if session.DownstreamCodeChallenge != encodedChallenge {
82
-
return nil, fmt.Errorf("invalid code challenge")
83
-
}
84
-
85
-
if session.DownstreamAuthorizationCode != tokenRequest.Code {
86
-
return nil, fmt.Errorf("invalid authorization code")
87
-
}
88
-
89
-
accessToken, err := o.generateJWT(session)
90
-
if err != nil {
91
-
return nil, fmt.Errorf("could not generate access token: %w", err)
92
-
}
93
-
94
-
refreshToken, err := generateRefreshToken()
95
-
if err != nil {
96
-
return nil, fmt.Errorf("could not generate refresh token: %w", err)
97
-
}
98
-
99
-
session.DownstreamAccessToken = accessToken
100
-
session.DownstreamRefreshToken = refreshToken
101
-
102
-
err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
103
-
if err != nil {
104
-
return nil, fmt.Errorf("could not update downstream session: %w", err)
105
-
}
106
-
107
-
return &TokenResponse{
108
-
AccessToken: accessToken,
109
-
TokenType: "DPoP",
110
-
RefreshToken: refreshToken,
111
-
Scope: "atproto transition:generic",
112
-
ExpiresIn: int(OAuthTokenExpiry.Seconds()),
113
-
Sub: session.DID,
114
-
}, nil
115
-
}
116
-
117
-
func (o *OProxy) RefreshToken(ctx context.Context, tokenRequest *TokenRequest, session *OAuthSession) (*TokenResponse, error) {
118
-
119
-
if session.Status() != OAuthSessionStateReady {
120
-
return nil, echo.NewHTTPError(http.StatusBadRequest, "session is not in ready state")
121
-
}
122
-
123
-
if session.DownstreamRefreshToken != tokenRequest.RefreshToken {
124
-
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid refresh token")
125
-
}
126
-
127
-
newJWT, err := o.generateJWT(session)
128
-
if err != nil {
129
-
return nil, fmt.Errorf("could not generate new access token: %w", err)
130
-
}
131
-
132
-
session.DownstreamAccessToken = newJWT
133
-
err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
134
-
if err != nil {
135
-
return nil, fmt.Errorf("could not update downstream session: %w", err)
136
-
}
137
-
138
-
return &TokenResponse{
139
-
AccessToken: newJWT,
140
-
TokenType: "DPoP",
141
-
RefreshToken: session.DownstreamRefreshToken,
142
-
Scope: "atproto transition:generic",
143
-
ExpiresIn: int(OAuthTokenExpiry.Seconds()),
144
-
Sub: session.DID,
145
-
}, nil
146
-
}
147
-
148
-
func (o *OProxy) Revoke(ctx context.Context, dpopHeader string, revokeRequest *RevokeRequest) error {
149
-
proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/revoke"}, dpop.ParseOptions{
150
-
Nonce: "",
151
-
TimeWindow: &dpopTimeWindow,
152
-
})
153
-
if err != nil {
154
-
return echo.NewHTTPError(http.StatusBadRequest, "invalid DPoP proof")
155
-
}
156
-
157
-
session, err := o.loadOAuthSession(proof.PublicKey())
158
-
if err != nil {
159
-
return fmt.Errorf("could not get downstream session: %w", err)
160
-
}
161
-
162
-
now := time.Now()
163
-
session.RevokedAt = &now
164
-
err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
165
-
if err != nil {
166
-
return fmt.Errorf("could not update downstream session: %w", err)
167
-
}
168
-
169
-
return nil
170
-
}
171
-
172
-
func (o *OProxy) generateJWT(session *OAuthSession) (string, error) {
173
-
uu, err := uuid.NewV7()
174
-
if err != nil {
175
-
return "", err
176
-
}
177
-
downstreamMeta, err := o.GetDownstreamMetadata("")
178
-
if err != nil {
179
-
return "", err
180
-
}
181
-
now := time.Now()
182
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
183
-
"jti": uu.String(),
184
-
"sub": session.DID,
185
-
"exp": now.Add(OAuthTokenExpiry).Unix(),
186
-
"iat": now.Unix(),
187
-
"nbf": now.Unix(),
188
-
"cnf": map[string]any{
189
-
"jkt": session.DownstreamDPoPJKT,
190
-
},
191
-
"aud": fmt.Sprintf("did:web:%s", o.host),
192
-
"scope": downstreamMeta.Scope,
193
-
"client_id": downstreamMeta.ClientID,
194
-
"iss": fmt.Sprintf("https://%s", o.host),
195
-
})
196
-
197
-
var rawKey any
198
-
if err := o.downstreamJWK.Raw(&rawKey); err != nil {
199
-
return "", err
200
-
}
201
-
202
-
tokenString, err := token.SignedString(rawKey)
203
-
204
-
if err != nil {
205
-
return "", err
206
-
}
207
-
208
-
return tokenString, nil
209
-
}
210
-
211
-
func (o *OProxy) DPoPNonceMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
212
-
return func(c echo.Context) error {
213
-
dpopHeader := c.Request().Header.Get("DPoP")
214
-
if dpopHeader == "" {
215
-
return echo.NewHTTPError(http.StatusBadRequest, "missing DPoP header")
216
-
}
217
-
218
-
jkt, _, err := getJKT(dpopHeader)
219
-
if err != nil {
220
-
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
221
-
}
222
-
223
-
session, err := o.loadOAuthSession(jkt)
224
-
if err != nil {
225
-
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
226
-
}
227
-
228
-
c.Set("session", session)
229
-
return next(c)
230
-
}
231
-
}
232
-
233
-
func (o *OProxy) ErrorHandlingMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
234
-
return func(c echo.Context) error {
235
-
err := next(c)
236
-
if err == nil {
237
-
return nil
238
-
}
239
-
httpError, ok := err.(*echo.HTTPError)
240
-
if ok {
241
-
o.slog.Error("oauth error", "code", httpError.Code, "message", httpError.Message, "internal", httpError.Internal)
242
-
return err
243
-
}
244
-
o.slog.Error("unhandled error", "error", err)
245
-
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
246
-
}
247
-
}
248
-
249
-
func generateRefreshToken() (string, error) {
250
-
uu, err := uuid.NewV7()
251
-
if err != nil {
252
-
return "", err
253
-
}
254
-
return fmt.Sprintf("refresh-%s", uu.String()), nil
255
-
}
256
-
257
-
func generateAuthorizationCode() (string, error) {
258
-
uu, err := uuid.NewV7()
259
-
if err != nil {
260
-
return "", err
261
-
}
262
-
return fmt.Sprintf("code-%s", uu.String()), nil
263
-
}
264
-
265
-
func generateOAuthServerMetadata(host string) map[string]any {
266
-
oauthServerMetadata := map[string]any{
267
-
"issuer": fmt.Sprintf("https://%s", host),
268
-
"request_parameter_supported": true,
269
-
"request_uri_parameter_supported": true,
270
-
"require_request_uri_registration": true,
271
-
"scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"},
272
-
"subject_types_supported": []string{"public"},
273
-
"response_types_supported": []string{"code"},
274
-
"response_modes_supported": []string{"query", "fragment", "form_post"},
275
-
"grant_types_supported": []string{"authorization_code", "refresh_token"},
276
-
"code_challenge_methods_supported": []string{"S256"},
277
-
"ui_locales_supported": []string{"en-US"},
278
-
"display_values_supported": []string{"page", "popup", "touch"},
279
-
"authorization_response_iss_parameter_supported": true,
280
-
"request_object_encryption_alg_values_supported": []string{},
281
-
"request_object_encryption_enc_values_supported": []string{},
282
-
"jwks_uri": fmt.Sprintf("https://%s/oauth/jwks", host),
283
-
"authorization_endpoint": fmt.Sprintf("https://%s/oauth/authorize", host),
284
-
"token_endpoint": fmt.Sprintf("https://%s/oauth/token", host),
285
-
"token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"},
286
-
"revocation_endpoint": fmt.Sprintf("https://%s/oauth/revoke", host),
287
-
"introspection_endpoint": fmt.Sprintf("https://%s/oauth/introspect", host),
288
-
"pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/oauth/par", host),
289
-
"require_pushed_authorization_requests": true,
290
-
"client_id_metadata_document_supported": true,
291
-
"request_object_signing_alg_values_supported": []string{
292
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
293
-
"ES256", "ES256K", "ES384", "ES512", "none",
294
-
},
295
-
"token_endpoint_auth_signing_alg_values_supported": []string{
296
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
297
-
"ES256", "ES256K", "ES384", "ES512",
298
-
},
299
-
"dpop_signing_alg_values_supported": []string{
300
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
301
-
"ES256", "ES256K", "ES384", "ES512",
302
-
},
303
-
}
304
-
return oauthServerMetadata
305
-
}
306
-
307
-
func (o *OProxy) GetDownstreamMetadata(redirectURI string) (*OAuthClientMetadata, error) {
308
-
meta := &OAuthClientMetadata{
309
-
ClientID: fmt.Sprintf("https://%s/oauth/downstream/client-metadata.json", o.host),
310
-
ClientURI: fmt.Sprintf("https://%s", o.host),
311
-
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
312
-
Scope: "atproto transition:generic",
313
-
TokenEndpointAuthMethod: "none",
314
-
ClientName: "Streamplace",
315
-
ResponseTypes: []string{"code"},
316
-
GrantTypes: []string{"authorization_code", "refresh_token"},
317
-
DPoPBoundAccessTokens: boolPtr(true),
318
-
RedirectURIs: []string{fmt.Sprintf("https://%s/login", o.host), fmt.Sprintf("https://%s/api/app-return", o.host)},
319
-
ApplicationType: "web",
320
-
}
321
-
if redirectURI != "" {
322
-
found := false
323
-
for _, uri := range meta.RedirectURIs {
324
-
if uri == redirectURI {
325
-
found = true
326
-
break
327
-
}
328
-
}
329
-
if !found {
330
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", redirectURI))
331
-
}
332
-
meta.RedirectURIs = []string{redirectURI}
333
-
}
334
-
return meta, nil
335
-
}
336
-
337
-
var ErrFirstNonce = echo.NewHTTPError(http.StatusBadRequest, "first time seeing this key, come back with a nonce")
338
-
339
-
func (o *OProxy) NewPAR(ctx context.Context, c echo.Context, par *PAR, dpopHeader string) (*PARResponse, error) {
340
-
jkt, nonce, err := getJKT(dpopHeader)
341
-
if err != nil {
342
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get JKT from DPoP header header=%s: %s", dpopHeader, err))
343
-
}
344
-
session, err := o.loadOAuthSession(jkt)
345
-
if err != nil {
346
-
return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to load OAuth session: %s", err))
347
-
}
348
-
// special case - if this is the first request, we need to send it back for a new nonce
349
-
if session == nil {
350
-
_, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{
351
-
Nonce: nonce, // normally this would be bad! but on the first request we're revalidating nonce anyway
352
-
TimeWindow: &dpopTimeWindow,
353
-
})
354
-
if err != nil {
355
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse DPoP header: %s", err))
356
-
}
357
-
newNonce := makeNonce()
358
-
err = o.createOAuthSession(jkt, &OAuthSession{
359
-
DownstreamDPoPJKT: jkt,
360
-
DownstreamDPoPNonce: newNonce,
361
-
})
362
-
if err != nil {
363
-
return nil, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth session: %s", err))
364
-
}
365
-
// come back later, nerd
366
-
c.Response().Header().Set("DPoP-Nonce", newNonce)
367
-
return nil, ErrFirstNonce
368
-
}
369
-
if session.DownstreamDPoPNonce != nonce {
370
-
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid nonce")
371
-
}
372
-
proof, err := dpop.Parse(dpopHeader, dpop.POST, &url.URL{Host: o.host, Scheme: "https", Path: "/oauth/par"}, dpop.ParseOptions{
373
-
Nonce: session.DownstreamDPoPNonce,
374
-
TimeWindow: &dpopTimeWindow,
375
-
})
376
-
// Check the error type to determine response
377
-
if err != nil {
378
-
// if ok := errors.Is(err, dpop.ErrInvalidProof); ok {
379
-
// apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", nil)
380
-
// return
381
-
// }
382
-
// apierrors.WriteHTTPBadRequest(w, "invalid DPoP proof", err)
383
-
// return
384
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid DPoP proof: %s", err))
385
-
}
386
-
if proof.PublicKey() != jkt {
387
-
panic("invalid code path: parsed DPoP proof twice and got different keys?!")
388
-
}
389
-
390
-
clientMetadata, err := o.GetDownstreamMetadata(par.RedirectURI)
391
-
if err != nil {
392
-
return nil, err
393
-
}
394
-
if par.ClientID != clientMetadata.ClientID {
395
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid client_id: expected %s, got %s", clientMetadata.ClientID, par.ClientID))
396
-
}
397
-
398
-
if !slices.Contains(clientMetadata.RedirectURIs, par.RedirectURI) {
399
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid redirect_uri: %s not in allowed URIs", par.RedirectURI))
400
-
}
401
-
402
-
if par.CodeChallengeMethod != "S256" {
403
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid code challenge method: expected S256, got %s", par.CodeChallengeMethod))
404
-
}
405
-
406
-
if par.ResponseMode != "query" {
407
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response mode: expected query, got %s", par.ResponseMode))
408
-
}
409
-
410
-
if par.ResponseType != "code" {
411
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid response type: expected code, got %s", par.ResponseType))
412
-
}
413
-
414
-
if par.Scope != o.scope {
415
-
return nil, echo.NewHTTPError(http.StatusBadRequest, "invalid scope")
416
-
}
417
-
418
-
if par.LoginHint == "" {
419
-
return nil, echo.NewHTTPError(http.StatusBadRequest, "login hint is required to find your PDS")
420
-
}
421
-
422
-
if par.State == "" {
423
-
return nil, echo.NewHTTPError(http.StatusBadRequest, "state is required")
424
-
}
425
-
426
-
if par.Scope != o.scope {
427
-
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid scope (expected %s, got %s)", o.scope, par.Scope))
428
-
}
429
-
430
-
urn := makeURN(jkt)
431
-
432
-
newNonce := makeNonce()
433
-
434
-
err = o.updateOAuthSession(jkt, &OAuthSession{
435
-
DownstreamDPoPJKT: jkt,
436
-
DownstreamDPoPNonce: newNonce,
437
-
DownstreamPARRequestURI: urn,
438
-
DownstreamCodeChallenge: par.CodeChallenge,
439
-
DownstreamState: par.State,
440
-
DownstreamRedirectURI: par.RedirectURI,
441
-
DID: par.LoginHint,
442
-
})
443
-
if err != nil {
444
-
return nil, fmt.Errorf("could not create oauth session: %w", err)
445
-
}
446
-
c.Response().Header().Set("DPoP-Nonce", newNonce)
447
-
448
-
resp := &PARResponse{
449
-
RequestURI: urn,
450
-
ExpiresIn: int(dpopTimeWindow.Seconds()),
451
-
}
452
-
453
-
return resp, nil
454
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+39
pkg/oproxy/oauth_middleware.go
···
14
15
"github.com/AxisCommunications/go-dpop"
16
"github.com/golang-jwt/jwt/v5"
0
17
)
18
19
var OAuthSessionContextKey = oauthSessionContextKeyType{}
···
193
194
return session, nil
195
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
14
15
"github.com/AxisCommunications/go-dpop"
16
"github.com/golang-jwt/jwt/v5"
17
+
"github.com/labstack/echo/v4"
18
)
19
20
var OAuthSessionContextKey = oauthSessionContextKeyType{}
···
194
195
return session, nil
196
}
197
+
198
+
func (o *OProxy) DPoPNonceMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
199
+
return func(c echo.Context) error {
200
+
dpopHeader := c.Request().Header.Get("DPoP")
201
+
if dpopHeader == "" {
202
+
return echo.NewHTTPError(http.StatusBadRequest, "missing DPoP header")
203
+
}
204
+
205
+
jkt, _, err := getJKT(dpopHeader)
206
+
if err != nil {
207
+
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
208
+
}
209
+
210
+
session, err := o.loadOAuthSession(jkt)
211
+
if err != nil {
212
+
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
213
+
}
214
+
215
+
c.Set("session", session)
216
+
return next(c)
217
+
}
218
+
}
219
+
220
+
func (o *OProxy) ErrorHandlingMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
221
+
return func(c echo.Context) error {
222
+
err := next(c)
223
+
if err == nil {
224
+
return nil
225
+
}
226
+
httpError, ok := err.(*echo.HTTPError)
227
+
if ok {
228
+
o.slog.Error("oauth error", "code", httpError.Code, "message", httpError.Message, "internal", httpError.Internal)
229
+
return err
230
+
}
231
+
o.slog.Error("unhandled error", "error", err)
232
+
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
233
+
}
234
+
}
-258
pkg/oproxy/oauth_upstream.go
···
1
-
package oproxy
2
-
3
-
import (
4
-
"context"
5
-
"encoding/json"
6
-
"fmt"
7
-
"net/http"
8
-
"net/url"
9
-
"time"
10
-
11
-
"github.com/bluesky-social/indigo/api/atproto"
12
-
"github.com/bluesky-social/indigo/xrpc"
13
-
oauth "github.com/haileyok/atproto-oauth-golang"
14
-
"github.com/haileyok/atproto-oauth-golang/helpers"
15
-
"github.com/labstack/echo/v4"
16
-
"github.com/lestrrat-go/jwx/v2/jwk"
17
-
)
18
-
19
-
// downstream --> upstream transition; attempt to send user to the upstream auth server
20
-
func (o *OProxy) Authorize(ctx context.Context, requestURI, clientID string) (string, error) {
21
-
downstreamMeta, err := o.GetDownstreamMetadata("")
22
-
if err != nil {
23
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get downstream metadata: %s", err))
24
-
}
25
-
if downstreamMeta.ClientID != clientID {
26
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("client ID mismatch: %s != %s", downstreamMeta.ClientID, clientID))
27
-
}
28
-
29
-
jkt, _, err := parseURN(requestURI)
30
-
if err != nil {
31
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse URN: %s", err))
32
-
}
33
-
34
-
session, err := o.loadOAuthSession(jkt)
35
-
if err != nil {
36
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to load OAuth session jkt=%s: %s", jkt, err))
37
-
}
38
-
39
-
if session == nil {
40
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no session found for jkt=%s", jkt))
41
-
}
42
-
43
-
if session.Status() != OAuthSessionStatePARCreated {
44
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in par-created state: %s", session.Status()))
45
-
}
46
-
47
-
if session.DownstreamPARRequestURI != requestURI {
48
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("request URI mismatch: %s != %s", session.DownstreamPARRequestURI, requestURI))
49
-
}
50
-
51
-
now := time.Now()
52
-
session.DownstreamPARUsedAt = &now
53
-
err = o.updateOAuthSession(jkt, session)
54
-
if err != nil {
55
-
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
56
-
}
57
-
58
-
upstreamMeta := o.GetUpstreamMetadata()
59
-
oclient, err := oauth.NewClient(oauth.ClientArgs{
60
-
ClientJwk: o.upstreamJWK,
61
-
ClientId: upstreamMeta.ClientID,
62
-
RedirectUri: upstreamMeta.RedirectURIs[0],
63
-
})
64
-
if err != nil {
65
-
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create OAuth client: %s", err))
66
-
}
67
-
68
-
did, err := resolveHandle(ctx, session.DID)
69
-
if err != nil {
70
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve handle '%s': %s", session.DID, err))
71
-
}
72
-
73
-
service, err := resolveService(ctx, did)
74
-
if err != nil {
75
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve service for DID '%s': %s", did, err))
76
-
}
77
-
78
-
authserver, err := oclient.ResolvePdsAuthServer(ctx, service)
79
-
if err != nil {
80
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to resolve PDS auth server for service '%s': %s", service, err))
81
-
}
82
-
83
-
authmeta, err := oclient.FetchAuthServerMetadata(ctx, authserver)
84
-
if err != nil {
85
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to fetch auth server metadata from '%s': %s", authserver, err))
86
-
}
87
-
88
-
k, err := helpers.GenerateKey(nil)
89
-
if err != nil {
90
-
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate DPoP key: %s", err))
91
-
}
92
-
93
-
state := makeState(jkt)
94
-
95
-
opts := oauth.ParAuthRequestOpts{
96
-
State: state,
97
-
}
98
-
parResp, err := oclient.SendParAuthRequest(ctx, authserver, authmeta, did, upstreamMeta.Scope, k, opts)
99
-
if err != nil {
100
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to send PAR auth request to '%s': %s", authserver, err))
101
-
}
102
-
103
-
jwkJSON, err := json.Marshal(k)
104
-
if err != nil {
105
-
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to marshal DPoP key to JSON: %s", err))
106
-
}
107
-
108
-
u, err := url.Parse(authmeta.AuthorizationEndpoint)
109
-
if err != nil {
110
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse auth server metadata: %s", err))
111
-
}
112
-
u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(upstreamMeta.ClientID), parResp.RequestUri)
113
-
str := u.String()
114
-
115
-
session.DID = did
116
-
session.PDSUrl = service
117
-
session.UpstreamState = parResp.State
118
-
session.UpstreamAuthServerIssuer = authserver
119
-
session.UpstreamPKCEVerifier = parResp.PkceVerifier
120
-
session.UpstreamDPoPNonce = parResp.DpopAuthserverNonce
121
-
session.UpstreamDPoPPrivateJWK = string(jwkJSON)
122
-
123
-
err = o.updateOAuthSession(jkt, session)
124
-
if err != nil {
125
-
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
126
-
}
127
-
128
-
return str, nil
129
-
}
130
-
131
-
func (o *OProxy) Return(ctx context.Context, code string, iss string, state string) (string, error) {
132
-
upstreamMeta := o.GetUpstreamMetadata()
133
-
oclient, err := oauth.NewClient(oauth.ClientArgs{
134
-
ClientJwk: o.upstreamJWK,
135
-
ClientId: upstreamMeta.ClientID,
136
-
RedirectUri: upstreamMeta.RedirectURIs[0],
137
-
})
138
-
139
-
jkt, _, err := parseState(state)
140
-
if err != nil {
141
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse state: %s", err))
142
-
}
143
-
144
-
session, err := o.loadOAuthSession(jkt)
145
-
if err != nil {
146
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to get OAuth session: %s", err))
147
-
}
148
-
if session == nil {
149
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no OAuth session found for state: %s", state))
150
-
}
151
-
152
-
if session.Status() != OAuthSessionStateUpstream {
153
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("session is not in upstream state: %s", session.Status()))
154
-
}
155
-
156
-
if session.UpstreamState != state {
157
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("state mismatch: %s != %s", session.UpstreamState, state))
158
-
}
159
-
160
-
if iss != session.UpstreamAuthServerIssuer {
161
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("issuer mismatch: %s != %s", iss, session.UpstreamAuthServerIssuer))
162
-
}
163
-
164
-
key, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK))
165
-
if err != nil {
166
-
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to parse DPoP private JWK: %s", err))
167
-
}
168
-
169
-
itResp, err := oclient.InitialTokenRequest(ctx, code, iss, session.UpstreamPKCEVerifier, session.UpstreamDPoPNonce, key)
170
-
if err != nil {
171
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to request initial token: %s", err))
172
-
}
173
-
now := time.Now()
174
-
175
-
if itResp.Sub != session.DID {
176
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("sub mismatch: %s != %s", itResp.Sub, session.DID))
177
-
}
178
-
179
-
if itResp.Scope != upstreamMeta.Scope {
180
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("scope mismatch: %s != %s", itResp.Scope, upstreamMeta.Scope))
181
-
}
182
-
183
-
downstreamCode, err := generateAuthorizationCode()
184
-
if err != nil {
185
-
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate downstream code: %s", err))
186
-
}
187
-
188
-
expiry := now.Add(time.Second * time.Duration(itResp.ExpiresIn)).UTC()
189
-
session.UpstreamAccessToken = itResp.AccessToken
190
-
session.UpstreamAccessTokenExp = &expiry
191
-
session.UpstreamRefreshToken = itResp.RefreshToken
192
-
session.DownstreamAuthorizationCode = downstreamCode
193
-
194
-
authArgs := &oauth.XrpcAuthedRequestArgs{
195
-
Did: session.DID,
196
-
AccessToken: session.UpstreamAccessToken,
197
-
PdsUrl: session.PDSUrl,
198
-
Issuer: session.UpstreamAuthServerIssuer,
199
-
DpopPdsNonce: session.UpstreamDPoPNonce,
200
-
DpopPrivateJwk: key,
201
-
}
202
-
203
-
xrpcClient := &oauth.XrpcClient{
204
-
OnDpopPdsNonceChanged: func(did, newNonce string) {},
205
-
}
206
-
207
-
// brief check to make sure we can actually do stuff
208
-
var out atproto.ServerCheckAccountStatus_Output
209
-
if err := xrpcClient.Do(ctx, authArgs, xrpc.Query, "application/json", "com.atproto.server.checkAccountStatus", nil, nil, &out); err != nil {
210
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to check account status: %s", err))
211
-
}
212
-
213
-
err = o.updateOAuthSession(session.DownstreamDPoPJKT, session)
214
-
if err != nil {
215
-
return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to update OAuth session: %s", err))
216
-
}
217
-
218
-
u, err := url.Parse(session.DownstreamRedirectURI)
219
-
if err != nil {
220
-
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to parse downstream redirect URI: %s", err))
221
-
}
222
-
q := u.Query()
223
-
q.Set("iss", fmt.Sprintf("https://%s", o.host))
224
-
q.Set("state", session.DownstreamState)
225
-
q.Set("code", session.DownstreamAuthorizationCode)
226
-
u.RawQuery = q.Encode()
227
-
228
-
return u.String(), nil
229
-
}
230
-
231
-
func (o *OProxy) GetUpstreamMetadata() *OAuthClientMetadata {
232
-
// publicKey, err := o.upstreamJWK.PublicKey()
233
-
// if err != nil {
234
-
// panic(err)
235
-
// }
236
-
// jwks := jwk.NewSet()
237
-
// err = jwks.AddKey(publicKey)
238
-
// if err != nil {
239
-
// panic(err)
240
-
// }
241
-
// ro := helpers.CreateJwksResponseObject(publicKey)
242
-
meta := &OAuthClientMetadata{
243
-
ClientID: fmt.Sprintf("https://%s/oauth/upstream/client-metadata.json", o.host),
244
-
JwksURI: fmt.Sprintf("https://%s/oauth/upstream/jwks.json", o.host),
245
-
ClientURI: fmt.Sprintf("https://%s", o.host),
246
-
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
247
-
Scope: "atproto transition:generic",
248
-
TokenEndpointAuthMethod: "private_key_jwt",
249
-
ClientName: "Streamplace",
250
-
ResponseTypes: []string{"code"},
251
-
GrantTypes: []string{"authorization_code", "refresh_token"},
252
-
DPoPBoundAccessTokens: boolPtr(true),
253
-
TokenEndpointAuthSigningAlg: "ES256",
254
-
RedirectURIs: []string{fmt.Sprintf("https://%s/oauth/return", o.host)},
255
-
// Jwks: ro,
256
-
}
257
-
return meta
258
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+11
pkg/oproxy/oproxy.go
···
2
3
import (
4
"log/slog"
0
5
"os"
6
7
"github.com/labstack/echo/v4"
···
61
o.e.Use(o.ErrorHandlingMiddleware)
62
return o
63
}
0
0
0
0
0
0
0
0
0
0
···
2
3
import (
4
"log/slog"
5
+
"net/http"
6
"os"
7
8
"github.com/labstack/echo/v4"
···
62
o.e.Use(o.ErrorHandlingMiddleware)
63
return o
64
}
65
+
66
+
func (o *OProxy) Handler() http.Handler {
67
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
68
+
w.Header().Set("Access-Control-Allow-Origin", "*") // todo: ehhhhhhhhhhhh
69
+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,DPoP")
70
+
w.Header().Set("Access-Control-Allow-Methods", "*")
71
+
w.Header().Set("Access-Control-Expose-Headers", "DPoP-Nonce")
72
+
o.e.ServeHTTP(w, r)
73
+
})
74
+
}
-18
pkg/oproxy/par.go
···
1
-
package oproxy
2
-
3
-
type PAR struct {
4
-
ClientID string `json:"client_id"`
5
-
RedirectURI string `json:"redirect_uri"`
6
-
CodeChallenge string `json:"code_challenge"`
7
-
CodeChallengeMethod string `json:"code_challenge_method"`
8
-
State string `json:"state"`
9
-
LoginHint string `json:"login_hint"`
10
-
ResponseMode string `json:"response_mode"`
11
-
ResponseType string `json:"response_type"`
12
-
Scope string `json:"scope"`
13
-
}
14
-
15
-
type PARResponse struct {
16
-
RequestURI string `json:"request_uri"`
17
-
ExpiresIn int `json:"expires_in"`
18
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+1
pkg/oproxy/resolution.go
···
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
)
14
0
15
func resolveHandle(ctx context.Context, handle string) (string, error) {
16
var did string
17
···
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
)
14
15
+
// mostly borrowed from github.com/haileyok/atproto-oauth-golang, MIT license
16
func resolveHandle(ctx context.Context, handle string) (string, error) {
17
var did string
18
+23
pkg/oproxy/token_generation.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package oproxy
2
+
3
+
import (
4
+
"fmt"
5
+
6
+
"github.com/google/uuid"
7
+
)
8
+
9
+
func generateRefreshToken() (string, error) {
10
+
uu, err := uuid.NewV7()
11
+
if err != nil {
12
+
return "", err
13
+
}
14
+
return fmt.Sprintf("refresh-%s", uu.String()), nil
15
+
}
16
+
17
+
func generateAuthorizationCode() (string, error) {
18
+
uu, err := uuid.NewV7()
19
+
if err != nil {
20
+
return "", err
21
+
}
22
+
return fmt.Sprintf("code-%s", uu.String()), nil
23
+
}