diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 93a644c303..38857b9c62 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1491,146 +1491,6 @@ const docTemplate = `{ } } }, - "/login/oauth2/authorize": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "tags": [ - "Enterprise" - ], - "summary": "OAuth2 authorization request.", - "operationId": "oauth2-authorization-request", - "parameters": [ - { - "type": "string", - "description": "Client ID", - "name": "client_id", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "A random unguessable string", - "name": "state", - "in": "query", - "required": true - }, - { - "enum": [ - "code" - ], - "type": "string", - "description": "Response type", - "name": "response_type", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Redirect here after authorization", - "name": "redirect_uri", - "in": "query" - }, - { - "type": "string", - "description": "Token scopes (currently ignored)", - "name": "scope", - "in": "query" - } - ], - "responses": { - "302": { - "description": "Found" - } - } - } - }, - "/login/oauth2/tokens": { - "post": { - "produces": [ - "application/json" - ], - "tags": [ - "Enterprise" - ], - "summary": "OAuth2 token exchange.", - "operationId": "oauth2-token-exchange", - "parameters": [ - { - "type": "string", - "description": "Client ID, required if grant_type=authorization_code", - "name": "client_id", - "in": "formData" - }, - { - "type": "string", - "description": "Client secret, required if grant_type=authorization_code", - "name": "client_secret", - "in": "formData" - }, - { - "type": "string", - "description": "Authorization code, required if grant_type=authorization_code", - "name": "code", - "in": "formData" - }, - { - "type": "string", - "description": "Refresh token, required if grant_type=refresh_token", - "name": "refresh_token", - "in": "formData" - }, - { - "enum": [ - "authorization_code", - "refresh_token" - ], - "type": "string", - "description": "Grant type", - "name": "grant_type", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/oauth2.Token" - } - } - } - }, - "delete": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "tags": [ - "Enterprise" - ], - "summary": "Delete OAuth2 application tokens.", - "operationId": "delete-oauth2-application-tokens", - "parameters": [ - { - "type": "string", - "description": "Client ID", - "name": "client_id", - "in": "query", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, "/oauth2-provider/apps": { "get": { "security": [ @@ -1915,6 +1775,146 @@ const docTemplate = `{ } } }, + "/oauth2/authorize": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Enterprise" + ], + "summary": "OAuth2 authorization request.", + "operationId": "oauth2-authorization-request", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "A random unguessable string", + "name": "state", + "in": "query", + "required": true + }, + { + "enum": [ + "code" + ], + "type": "string", + "description": "Response type", + "name": "response_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Redirect here after authorization", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Token scopes (currently ignored)", + "name": "scope", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/oauth2/tokens": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Enterprise" + ], + "summary": "OAuth2 token exchange.", + "operationId": "oauth2-token-exchange", + "parameters": [ + { + "type": "string", + "description": "Client ID, required if grant_type=authorization_code", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Client secret, required if grant_type=authorization_code", + "name": "client_secret", + "in": "formData" + }, + { + "type": "string", + "description": "Authorization code, required if grant_type=authorization_code", + "name": "code", + "in": "formData" + }, + { + "type": "string", + "description": "Refresh token, required if grant_type=refresh_token", + "name": "refresh_token", + "in": "formData" + }, + { + "enum": [ + "authorization_code", + "refresh_token" + ], + "type": "string", + "description": "Grant type", + "name": "grant_type", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/oauth2.Token" + } + } + } + }, + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Enterprise" + ], + "summary": "Delete OAuth2 application tokens.", + "operationId": "delete-oauth2-application-tokens", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/organizations": { "post": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c243356f88..11dad829c4 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1291,133 +1291,6 @@ } } }, - "/login/oauth2/authorize": { - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "tags": ["Enterprise"], - "summary": "OAuth2 authorization request.", - "operationId": "oauth2-authorization-request", - "parameters": [ - { - "type": "string", - "description": "Client ID", - "name": "client_id", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "A random unguessable string", - "name": "state", - "in": "query", - "required": true - }, - { - "enum": ["code"], - "type": "string", - "description": "Response type", - "name": "response_type", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Redirect here after authorization", - "name": "redirect_uri", - "in": "query" - }, - { - "type": "string", - "description": "Token scopes (currently ignored)", - "name": "scope", - "in": "query" - } - ], - "responses": { - "302": { - "description": "Found" - } - } - } - }, - "/login/oauth2/tokens": { - "post": { - "produces": ["application/json"], - "tags": ["Enterprise"], - "summary": "OAuth2 token exchange.", - "operationId": "oauth2-token-exchange", - "parameters": [ - { - "type": "string", - "description": "Client ID, required if grant_type=authorization_code", - "name": "client_id", - "in": "formData" - }, - { - "type": "string", - "description": "Client secret, required if grant_type=authorization_code", - "name": "client_secret", - "in": "formData" - }, - { - "type": "string", - "description": "Authorization code, required if grant_type=authorization_code", - "name": "code", - "in": "formData" - }, - { - "type": "string", - "description": "Refresh token, required if grant_type=refresh_token", - "name": "refresh_token", - "in": "formData" - }, - { - "enum": ["authorization_code", "refresh_token"], - "type": "string", - "description": "Grant type", - "name": "grant_type", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/oauth2.Token" - } - } - } - }, - "delete": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "tags": ["Enterprise"], - "summary": "Delete OAuth2 application tokens.", - "operationId": "delete-oauth2-application-tokens", - "parameters": [ - { - "type": "string", - "description": "Client ID", - "name": "client_id", - "in": "query", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, "/oauth2-provider/apps": { "get": { "security": [ @@ -1670,6 +1543,133 @@ } } }, + "/oauth2/authorize": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Enterprise"], + "summary": "OAuth2 authorization request.", + "operationId": "oauth2-authorization-request", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "A random unguessable string", + "name": "state", + "in": "query", + "required": true + }, + { + "enum": ["code"], + "type": "string", + "description": "Response type", + "name": "response_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Redirect here after authorization", + "name": "redirect_uri", + "in": "query" + }, + { + "type": "string", + "description": "Token scopes (currently ignored)", + "name": "scope", + "in": "query" + } + ], + "responses": { + "302": { + "description": "Found" + } + } + } + }, + "/oauth2/tokens": { + "post": { + "produces": ["application/json"], + "tags": ["Enterprise"], + "summary": "OAuth2 token exchange.", + "operationId": "oauth2-token-exchange", + "parameters": [ + { + "type": "string", + "description": "Client ID, required if grant_type=authorization_code", + "name": "client_id", + "in": "formData" + }, + { + "type": "string", + "description": "Client secret, required if grant_type=authorization_code", + "name": "client_secret", + "in": "formData" + }, + { + "type": "string", + "description": "Authorization code, required if grant_type=authorization_code", + "name": "code", + "in": "formData" + }, + { + "type": "string", + "description": "Refresh token, required if grant_type=refresh_token", + "name": "refresh_token", + "in": "formData" + }, + { + "enum": ["authorization_code", "refresh_token"], + "type": "string", + "description": "Grant type", + "name": "grant_type", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/oauth2.Token" + } + } + } + }, + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Enterprise"], + "summary": "Delete OAuth2 application tokens.", + "operationId": "delete-oauth2-application-tokens", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "client_id", + "in": "query", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/organizations": { "post": { "security": [ diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 005523b86c..5389f263ab 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -236,10 +236,10 @@ func OAuth2ProviderApp(accessURL *url.URL, dbApp database.OAuth2ProviderApp) cod Icon: dbApp.Icon, Endpoints: codersdk.OAuth2AppEndpoints{ Authorization: accessURL.ResolveReference(&url.URL{ - Path: "/login/oauth2/authorize", + Path: "/oauth2/authorize", }).String(), Token: accessURL.ResolveReference(&url.URL{ - Path: "/login/oauth2/tokens", + Path: "/oauth2/tokens", }).String(), // We do not currently support DeviceAuth. DeviceAuth: "", diff --git a/codersdk/oauth2.go b/codersdk/oauth2.go index d4d20c26b1..74f12bd303 100644 --- a/codersdk/oauth2.go +++ b/codersdk/oauth2.go @@ -213,7 +213,7 @@ func (e OAuth2ProviderResponseType) Valid() bool { // RevokeOAuth2ProviderApp completely revokes an app's access for the // authenticated user. func (c *Client) RevokeOAuth2ProviderApp(ctx context.Context, appID uuid.UUID) error { - res, err := c.Request(ctx, http.MethodDelete, "/login/oauth2/tokens", nil, func(r *http.Request) { + res, err := c.Request(ctx, http.MethodDelete, "/oauth2/tokens", nil, func(r *http.Request) { q := r.URL.Query() q.Set("client_id", appID.String()) r.URL.RawQuery = q.Encode() diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index 3e80637666..88b728421c 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -534,127 +534,6 @@ curl -X DELETE http://coder-server:8080/api/v2/licenses/{id} \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## OAuth2 authorization request. - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/login/oauth2/authorize?client_id=string&state=string&response_type=code \ - -H 'Coder-Session-Token: API_KEY' -``` - -`POST /login/oauth2/authorize` - -### Parameters - -| Name | In | Type | Required | Description | -| --------------- | ----- | ------ | -------- | --------------------------------- | -| `client_id` | query | string | true | Client ID | -| `state` | query | string | true | A random unguessable string | -| `response_type` | query | string | true | Response type | -| `redirect_uri` | query | string | false | Redirect here after authorization | -| `scope` | query | string | false | Token scopes (currently ignored) | - -#### Enumerated Values - -| Parameter | Value | -| --------------- | ------ | -| `response_type` | `code` | - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ---------------------------------------------------------- | ----------- | ------ | -| 302 | [Found](https://tools.ietf.org/html/rfc7231#section-6.4.3) | Found | | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## OAuth2 token exchange. - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/login/oauth2/tokens \ - -H 'Accept: application/json' -``` - -`POST /login/oauth2/tokens` - -> Body parameter - -```yaml -client_id: string -client_secret: string -code: string -refresh_token: string -grant_type: authorization_code -``` - -### Parameters - -| Name | In | Type | Required | Description | -| ----------------- | ---- | ------ | -------- | ------------------------------------------------------------- | -| `body` | body | object | false | | -| `» client_id` | body | string | false | Client ID, required if grant_type=authorization_code | -| `» client_secret` | body | string | false | Client secret, required if grant_type=authorization_code | -| `» code` | body | string | false | Authorization code, required if grant_type=authorization_code | -| `» refresh_token` | body | string | false | Refresh token, required if grant_type=refresh_token | -| `» grant_type` | body | string | true | Grant type | - -#### Enumerated Values - -| Parameter | Value | -| -------------- | -------------------- | -| `» grant_type` | `authorization_code` | -| `» grant_type` | `refresh_token` | - -### Example responses - -> 200 Response - -```json -{ - "access_token": "string", - "expiry": "string", - "refresh_token": "string", - "token_type": "string" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | -------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [oauth2.Token](schemas.md#oauth2token) | - -## Delete OAuth2 application tokens. - -### Code samples - -```shell -# Example request using curl -curl -X DELETE http://coder-server:8080/api/v2/login/oauth2/tokens?client_id=string \ - -H 'Coder-Session-Token: API_KEY' -``` - -`DELETE /login/oauth2/tokens` - -### Parameters - -| Name | In | Type | Required | Description | -| ----------- | ----- | ------ | -------- | ----------- | -| `client_id` | query | string | true | Client ID | - -### Responses - -| Status | Meaning | Description | Schema | -| ------ | --------------------------------------------------------------- | ----------- | ------ | -| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Get OAuth2 applications. ### Code samples @@ -1031,6 +910,127 @@ curl -X DELETE http://coder-server:8080/api/v2/oauth2-provider/apps/{app}/secret To perform this operation, you must be authenticated. [Learn more](authentication.md). +## OAuth2 authorization request. + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/oauth2/authorize?client_id=string&state=string&response_type=code \ + -H 'Coder-Session-Token: API_KEY' +``` + +`POST /oauth2/authorize` + +### Parameters + +| Name | In | Type | Required | Description | +| --------------- | ----- | ------ | -------- | --------------------------------- | +| `client_id` | query | string | true | Client ID | +| `state` | query | string | true | A random unguessable string | +| `response_type` | query | string | true | Response type | +| `redirect_uri` | query | string | false | Redirect here after authorization | +| `scope` | query | string | false | Token scopes (currently ignored) | + +#### Enumerated Values + +| Parameter | Value | +| --------------- | ------ | +| `response_type` | `code` | + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ---------------------------------------------------------- | ----------- | ------ | +| 302 | [Found](https://tools.ietf.org/html/rfc7231#section-6.4.3) | Found | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## OAuth2 token exchange. + +### Code samples + +```shell +# Example request using curl +curl -X POST http://coder-server:8080/api/v2/oauth2/tokens \ + -H 'Accept: application/json' +``` + +`POST /oauth2/tokens` + +> Body parameter + +```yaml +client_id: string +client_secret: string +code: string +refresh_token: string +grant_type: authorization_code +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ----------------- | ---- | ------ | -------- | ------------------------------------------------------------- | +| `body` | body | object | false | | +| `» client_id` | body | string | false | Client ID, required if grant_type=authorization_code | +| `» client_secret` | body | string | false | Client secret, required if grant_type=authorization_code | +| `» code` | body | string | false | Authorization code, required if grant_type=authorization_code | +| `» refresh_token` | body | string | false | Refresh token, required if grant_type=refresh_token | +| `» grant_type` | body | string | true | Grant type | + +#### Enumerated Values + +| Parameter | Value | +| -------------- | -------------------- | +| `» grant_type` | `authorization_code` | +| `» grant_type` | `refresh_token` | + +### Example responses + +> 200 Response + +```json +{ + "access_token": "string", + "expiry": "string", + "refresh_token": "string", + "token_type": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [oauth2.Token](schemas.md#oauth2token) | + +## Delete OAuth2 application tokens. + +### Code samples + +```shell +# Example request using curl +curl -X DELETE http://coder-server:8080/api/v2/oauth2/tokens?client_id=string \ + -H 'Coder-Session-Token: API_KEY' +``` + +`DELETE /oauth2/tokens` + +### Parameters + +| Name | In | Type | Required | Description | +| ----------- | ----- | ------ | -------- | ----------- | +| `client_id` | query | string | true | Client ID | + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | --------------------------------------------------------------- | ----------- | ------ | +| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get groups by organization ### Code samples diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 3c90b9992c..7952d3c464 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -168,24 +168,25 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { } api.AGPL.RootHandler.Group(func(r chi.Router) { - r.Use( - api.oAuth2ProviderMiddleware, - // Fetch the app as system because in the /tokens route there will be no - // authenticated user. - httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderApp(options.Database)), - ) // Oauth2 linking routes do not make sense under the /api/v2 path. - r.Route("/login", func(r chi.Router) { - r.Route("/oauth2", func(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(apiKeyMiddleware) - r.Get("/authorize", api.postOAuth2ProviderAppAuthorize()) - r.Delete("/tokens", api.deleteOAuth2ProviderAppTokens()) - }) - // The /tokens endpoint will be called from an unauthorized client so we - // cannot require an API key. - r.Post("/tokens", api.postOAuth2ProviderAppToken()) + r.Route("/oauth2", func(r chi.Router) { + r.Use( + api.oAuth2ProviderMiddleware, + // Fetch the app as system because in the /tokens route there will be no + // authenticated user. + httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderApp(options.Database)), + ) + r.Group(func(r chi.Router) { + r.Use(apiKeyMiddleware) + r.Get("/authorize", api.postOAuth2ProviderAppAuthorize()) + // DELETE on /tokens is not part of the OAuth2 spec. It is our own + // route used to revoke permissions from an application. It is here for + // parity with POST on /tokens. + r.Delete("/tokens", api.deleteOAuth2ProviderAppTokens()) }) + // The /tokens endpoint will be called from an unauthorized client so we + // cannot require an API key. + r.Post("/tokens", api.postOAuth2ProviderAppToken()) }) }) diff --git a/enterprise/coderd/identityprovider/middleware.go b/enterprise/coderd/identityprovider/middleware.go index 640ea8652e..663e9f608e 100644 --- a/enterprise/coderd/identityprovider/middleware.go +++ b/enterprise/coderd/identityprovider/middleware.go @@ -44,7 +44,7 @@ func authorizeMW(accessURL *url.URL) func(next http.Handler) http.Handler { // a minimum in order to detect whether "allow" has been pressed, however. cameFromSelf := (origin == "" || originU.Hostname() == accessURL.Hostname()) && refererU.Hostname() == accessURL.Hostname() && - refererU.Path == "/login/oauth2/authorize" + refererU.Path == "/oauth2/authorize" // If we were redirected here from this same page it means the user // pressed the allow button so defer to the authorize handler which diff --git a/enterprise/coderd/oauth2.go b/enterprise/coderd/oauth2.go index 0f016d6533..9455e0bdfa 100644 --- a/enterprise/coderd/oauth2.go +++ b/enterprise/coderd/oauth2.go @@ -291,7 +291,7 @@ func (api *API) deleteOAuth2ProviderAppSecret(rw http.ResponseWriter, r *http.Re // @Param redirect_uri query string false "Redirect here after authorization" // @Param scope query string false "Token scopes (currently ignored)" // @Success 302 -// @Router /login/oauth2/authorize [post] +// @Router /oauth2/authorize [post] func (api *API) postOAuth2ProviderAppAuthorize() http.HandlerFunc { return identityprovider.Authorize(api.Database, api.AccessURL) } @@ -306,7 +306,7 @@ func (api *API) postOAuth2ProviderAppAuthorize() http.HandlerFunc { // @Param refresh_token formData string false "Refresh token, required if grant_type=refresh_token" // @Param grant_type formData codersdk.OAuth2ProviderGrantType true "Grant type" // @Success 200 {object} oauth2.Token -// @Router /login/oauth2/tokens [post] +// @Router /oauth2/tokens [post] func (api *API) postOAuth2ProviderAppToken() http.HandlerFunc { return identityprovider.Tokens(api.Database, api.DeploymentValues.SessionDuration.Value()) } @@ -317,7 +317,7 @@ func (api *API) postOAuth2ProviderAppToken() http.HandlerFunc { // @Tags Enterprise // @Param client_id query string true "Client ID" // @Success 204 -// @Router /login/oauth2/tokens [delete] +// @Router /oauth2/tokens [delete] func (api *API) deleteOAuth2ProviderAppTokens() http.HandlerFunc { return identityprovider.RevokeApp(api.Database) } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 56f9c303f2..c3bde07743 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3411,8 +3411,8 @@ export const MockOAuth2ProviderApps: TypesGen.OAuth2ProviderApp[] = [ callback_url: "http://localhost:3001", icon: "/icon/github.svg", endpoints: { - authorization: "http://localhost:3001/login/oauth2/authorize", - token: "http://localhost:3001/login/oauth2/token", + authorization: "http://localhost:3001/oauth2/authorize", + token: "http://localhost:3001/oauth2/token", device_authorization: "", }, },