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