summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2023-09-07 11:27:41 -0700
committerMike Crute <mike@crute.us>2023-09-07 11:27:46 -0700
commit691d7abfdf5e8aa057483a1eb4340c71e45253de (patch)
treeb816fa567344aaf9529cfaa24031502bb0fcad6f
parentcc8afd651957d7409868fc1d7bde599af188d8cd (diff)
downloadwebsocket_proxy-master.tar.bz2
websocket_proxy-master.tar.xz
websocket_proxy-master.zip
Support Oauth2 discoveryHEADmaster
-rw-r--r--app/controllers/oauth2_discovery.go27
-rw-r--r--app/models/oauth2.go47
-rw-r--r--cmd/client/oauth2.go51
-rw-r--r--cmd/web/server.go15
4 files changed, 133 insertions, 7 deletions
diff --git a/app/controllers/oauth2_discovery.go b/app/controllers/oauth2_discovery.go
new file mode 100644
index 0000000..15528e6
--- /dev/null
+++ b/app/controllers/oauth2_discovery.go
@@ -0,0 +1,27 @@
1package controllers
2
3import (
4 "fmt"
5 "net/http"
6
7 "code.crute.us/mcrute/ssh-proxy/app/models"
8 "github.com/labstack/echo/v4"
9)
10
11type Oauth2DiscoveryController struct {
12 Hostname string
13}
14
15func (d *Oauth2DiscoveryController) Handle(c echo.Context) error {
16 return c.JSON(http.StatusOK, models.OauthDiscoveryMetadata{
17 Issuer: d.Hostname,
18 AuthorizationEndpoint: fmt.Sprintf("%s/auth/login", d.Hostname), // Not really supported here
19 TokenEndpoint: fmt.Sprintf("%s/auth/token", d.Hostname),
20 DeviceAuthorizationEndpoint: fmt.Sprintf("%s/auth/device", d.Hostname),
21 SupportedResponseTypes: []string{models.ResponseTypeCode},
22 SupportedGrantTypes: []string{models.GrantTypeDevice},
23 SupportedResponseModes: []string{models.ResponseModeQuery},
24 SupportedUILocales: []string{"en-us"},
25 SupportedChallengeCodeMethods: []string{models.ChallengeTypeSHA256},
26 })
27}
diff --git a/app/models/oauth2.go b/app/models/oauth2.go
index 9bfde0a..65d37d4 100644
--- a/app/models/oauth2.go
+++ b/app/models/oauth2.go
@@ -101,3 +101,50 @@ func (c *PKCEChallenge) Challenge() string {
101func (c *PKCEChallenge) EqualString(o string) bool { 101func (c *PKCEChallenge) EqualString(o string) bool {
102 return subtle.ConstantTimeCompare([]byte(o), []byte(c.Challenge())) != 1 102 return subtle.ConstantTimeCompare([]byte(o), []byte(c.Challenge())) != 1
103} 103}
104
105const (
106 GrantTypeAuthCode = "authorization_code" // RFC7591
107 GrantTypeImplicit = "implicit" // RFC7591
108 GrantTypePassword = "password" // RFC7591
109 GrantTypeClientCreds = "client_credentials" // RFC7591
110 GrantTypeRefreshToken = "refresh_token" // RFC7591
111 GrantTypeBearerJwt = "urn:ietf:params:oauth:grant-type:jwt-bearer" // RFC7591
112 GrantTypeBearerSaml = "urn:ietf:params:oauth:grant-type:saml2-bearer" // RFC7591
113 GrantTypeDevice = "urn:ietf:params:oauth:grant-type:device_code" // RFC8628
114 ResponseTypeCode = "code" // RFC7591
115 ResponseTypeToken = "token" // RFC7591
116 ResponseModeQuery = "query" // RFC7591
117 ResponseModeFragment = "fragment" // RFC7591
118 ResponseModeFormPost = "form_post" // RFC7591
119 ChallengeTypePlain = "plain" // RFC7636
120 ChallengeTypeSHA256 = "S256" // RFC7636
121 Oauth2MetadataPath = "/.well-known/oauth-authorization-server"
122 Oauth2MetadataCompatPath = "/.well-known/openid-configuration"
123)
124
125// All options are required unless omitempty
126type OauthDiscoveryMetadata struct {
127 Issuer string `json:"issuer"` // RFC88414, https url w/no query/fragment
128 AuthorizationEndpoint string `json:"authorization_endpoint"` // RFC88414
129 TokenEndpoint string `json:"token_endpoint"` // RFC88414
130 SupportedResponseTypes []string `json:"response_types_supported"` // RFC88414
131 JWKSUri string `json:"jwks_uri,omitempty"` // RFC88414
132 RegistrationEndpoint string `json:"registration_endpoint,omitempty"` // RFC88414
133 SupportedScopes []string `json:"scopes_supported,omitempty"` // RFC88414
134 SupportedResponseModes []string `json:"response_modes_supported,omitempty"` // RFC88414
135 SupportedGrantTypes []string `json:"grant_types_supported,omitempty"` // RFC88414, default: authorization_code, implicit
136 SupportedAuthMethods []string `json:"token_endpoint_auth_methods_supported,omitempty"` // RFC88414
137 SupportedSigningAlgs []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"` // RFC88414
138 SupportedUILocales []string `json:"ui_locales_supported,omitempty"` // RFC88414, RFC5646 codes
139 PolicyUri string `json:"op_policy_uri,omitempty"` // RFC88414
140 TosUri string `json:"op_tos_uri,omitempty"` // RFC88414
141 RevocationEndpoint string `json:"revocation_endpoint,omitempty"` // RFC88414
142 SupportedRevocationAuthMethods []string `json:"revocation_endpoint_auth_methods_supported,omitempty"` // RFC88414
143 SupportedRevocationSigningAlgs []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"` // RFC88414
144 IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` // RFC88414
145 SupportedIntrospectionAuthMethods []string `json:"introspection_endpoint_auth_methods_supported,omitempty"` // RFC88414
146 SupportedIntrospectionSigningAlgs []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"` // RFC88414
147 SupportedChallengeCodeMethods []string `json:"code_challenge_methods_supported,omitempty"` // RFC88414
148 ServiceDocumentation string `json:"service_documentation,omitempty"` // RFC88414
149 DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint,omitempty"` // RFC8628
150}
diff --git a/cmd/client/oauth2.go b/cmd/client/oauth2.go
index 6667c5a..1ccdaaa 100644
--- a/cmd/client/oauth2.go
+++ b/cmd/client/oauth2.go
@@ -5,6 +5,7 @@ import (
5 "encoding/json" 5 "encoding/json"
6 "fmt" 6 "fmt"
7 "net/http" 7 "net/http"
8 "net/url"
8 "strings" 9 "strings"
9 "time" 10 "time"
10 11
@@ -23,6 +24,36 @@ type Oauth2PKCEDeviceClient struct {
23 interval time.Duration 24 interval time.Duration
24} 25}
25 26
27func (c *Oauth2PKCEDeviceClient) discoverHost(ctx context.Context) (*models.OauthDiscoveryMetadata, error) {
28 u := &url.URL{
29 Scheme: "https",
30 Host: c.Host,
31 Path: models.Oauth2MetadataPath,
32 }
33
34 req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
35 if err != nil {
36 return nil, err
37 }
38
39 res, err := http.DefaultClient.Do(req)
40 if err != nil {
41 return nil, err
42 }
43 defer res.Body.Close()
44
45 if res.StatusCode != http.StatusOK {
46 return nil, fmt.Errorf("Oauth2 discovery request failed with code %d", res.StatusCode)
47 }
48
49 var resp models.OauthDiscoveryMetadata
50 if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
51 return nil, err
52 }
53
54 return &resp, nil
55}
56
26func (c *Oauth2PKCEDeviceClient) Authorize(ctx context.Context) (*models.DeviceAuthorizationResponse, error) { 57func (c *Oauth2PKCEDeviceClient) Authorize(ctx context.Context) (*models.DeviceAuthorizationResponse, error) {
27 challenge, err := models.NewPKCEChallenge() 58 challenge, err := models.NewPKCEChallenge()
28 if err != nil { 59 if err != nil {
@@ -40,8 +71,12 @@ func (c *Oauth2PKCEDeviceClient) Authorize(ctx context.Context) (*models.DeviceA
40 return nil, err 71 return nil, err
41 } 72 }
42 73
43 url := fmt.Sprintf("https://%s/auth/device", c.Host) 74 md, err := c.discoverHost(ctx)
44 req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(values.Encode())) 75 if err != nil {
76 return nil, err
77 }
78
79 req, err := http.NewRequestWithContext(ctx, http.MethodPost, md.DeviceAuthorizationEndpoint, strings.NewReader(values.Encode()))
45 if err != nil { 80 if err != nil {
46 return nil, err 81 return nil, err
47 } 82 }
@@ -53,7 +88,7 @@ func (c *Oauth2PKCEDeviceClient) Authorize(ctx context.Context) (*models.DeviceA
53 } 88 }
54 defer res.Body.Close() 89 defer res.Body.Close()
55 90
56 if res.StatusCode != 200 { 91 if res.StatusCode != http.StatusOK {
57 var resError models.Oauth2Error 92 var resError models.Oauth2Error
58 if err := json.NewDecoder(res.Body).Decode(&resError); err != nil { 93 if err := json.NewDecoder(res.Body).Decode(&resError); err != nil {
59 return nil, err 94 return nil, err
@@ -85,8 +120,12 @@ func (c *Oauth2PKCEDeviceClient) fetchToken(ctx context.Context, deviceCode stri
85 return nil, err 120 return nil, err
86 } 121 }
87 122
88 url := fmt.Sprintf("https://%s/auth/token", c.Host) 123 md, err := c.discoverHost(ctx)
89 req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(values.Encode())) 124 if err != nil {
125 return nil, err
126 }
127
128 req, err := http.NewRequestWithContext(ctx, http.MethodPost, md.TokenEndpoint, strings.NewReader(values.Encode()))
90 if err != nil { 129 if err != nil {
91 return nil, err 130 return nil, err
92 } 131 }
@@ -98,7 +137,7 @@ func (c *Oauth2PKCEDeviceClient) fetchToken(ctx context.Context, deviceCode stri
98 } 137 }
99 defer res.Body.Close() 138 defer res.Body.Close()
100 139
101 if res.StatusCode != 200 { 140 if res.StatusCode != http.StatusOK {
102 var resError models.Oauth2Error 141 var resError models.Oauth2Error
103 if err := json.NewDecoder(res.Body).Decode(&resError); err != nil { 142 if err := json.NewDecoder(res.Body).Decode(&resError); err != nil {
104 return nil, err 143 return nil, err
diff --git a/cmd/web/server.go b/cmd/web/server.go
index e930257..4867970 100644
--- a/cmd/web/server.go
+++ b/cmd/web/server.go
@@ -171,15 +171,25 @@ func webMain(cfg app.Config, embeddedTemplates, embeddedClients fs.FS, appVersio
171 Webauthn: wauthn, 171 Webauthn: wauthn,
172 } 172 }
173 173
174 // TODO: Clean up this hack and expose these to echo
175 hostname := fmt.Sprintf("https://%s", cfg.Hostnames[0])
176 if strings.HasPrefix(cfg.Hostnames[0], "dev.") {
177 hostname += ":8070"
178 }
179
174 o2dc := &controllers.OAuth2DeviceController[*app.Session]{ 180 o2dc := &controllers.OAuth2DeviceController[*app.Session]{
175 Logger: s.Logger, 181 Logger: s.Logger,
176 AuthSessions: authSessionStore, 182 AuthSessions: authSessionStore,
177 OauthClients: oauthClientStore, 183 OauthClients: oauthClientStore,
178 Hostname: fmt.Sprintf("https://%s", cfg.Hostnames[0]), // TODO 184 Hostname: hostname,
179 PollSeconds: cfg.OauthDevicePollSecs, 185 PollSeconds: cfg.OauthDevicePollSecs,
180 SessionExpiration: cfg.OauthSessionTimeout, 186 SessionExpiration: cfg.OauthSessionTimeout,
181 } 187 }
182 188
189 od := controllers.Oauth2DiscoveryController{
190 Hostname: hostname,
191 }
192
183 ph := &controllers.ProxyHandler{ 193 ph := &controllers.ProxyHandler{
184 Logger: s.Logger, 194 Logger: s.Logger,
185 Users: userStore, 195 Users: userStore,
@@ -260,5 +270,8 @@ func webMain(cfg app.Config, embeddedTemplates, embeddedClients fs.FS, appVersio
260 pg.GET("/:host/:port", ph.Handle) 270 pg.GET("/:host/:port", ph.Handle)
261 } 271 }
262 272
273 s.GET(models.Oauth2MetadataPath, od.Handle)
274 s.GET(models.Oauth2MetadataCompatPath, od.Handle)
275
263 s.RunForever(!cfg.DisableBackgroundJobs) 276 s.RunForever(!cfg.DisableBackgroundJobs)
264} 277}