diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 06ed3e19dfe1c..e17e2d8081180 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9688,6 +9688,21 @@ const docTemplate = `{ } } }, + "codersdk.OAuth2AppEndpoints": { + "type": "object", + "properties": { + "authorization": { + "type": "string" + }, + "device_authorization": { + "description": "DeviceAuth is optional.", + "type": "string" + }, + "token": { + "type": "string" + } + } + }, "codersdk.OAuth2Config": { "type": "object", "properties": { @@ -9734,6 +9749,14 @@ const docTemplate = `{ "callback_url": { "type": "string" }, + "endpoints": { + "description": "Endpoints are included in the app response for easier discovery. The OAuth2\nspec does not have a defined place to find these (for comparison, OIDC has\na '/.well-known/openid-configuration' endpoint).", + "allOf": [ + { + "$ref": "#/definitions/codersdk.OAuth2AppEndpoints" + } + ] + }, "icon": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8982d4a4a781f..34b4bd36df2a3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8683,6 +8683,21 @@ } } }, + "codersdk.OAuth2AppEndpoints": { + "type": "object", + "properties": { + "authorization": { + "type": "string" + }, + "device_authorization": { + "description": "DeviceAuth is optional.", + "type": "string" + }, + "token": { + "type": "string" + } + } + }, "codersdk.OAuth2Config": { "type": "object", "properties": { @@ -8729,6 +8744,14 @@ "callback_url": { "type": "string" }, + "endpoints": { + "description": "Endpoints are included in the app response for easier discovery. The OAuth2\nspec does not have a defined place to find these (for comparison, OIDC has\na '/.well-known/openid-configuration' endpoint).", + "allOf": [ + { + "$ref": "#/definitions/codersdk.OAuth2AppEndpoints" + } + ] + }, "icon": { "type": "string" }, diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index c88b8d5c8a685..3d9953dd874b1 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -4,6 +4,7 @@ package db2sdk import ( "encoding/json" "fmt" + "net/url" "strconv" "strings" "time" @@ -226,19 +227,29 @@ func templateVersionParameterOptions(rawOptions json.RawMessage) ([]codersdk.Tem return options, nil } -func OAuth2ProviderApp(dbApp database.OAuth2ProviderApp) codersdk.OAuth2ProviderApp { +func OAuth2ProviderApp(accessURL *url.URL, dbApp database.OAuth2ProviderApp) codersdk.OAuth2ProviderApp { return codersdk.OAuth2ProviderApp{ ID: dbApp.ID, Name: dbApp.Name, CallbackURL: dbApp.CallbackURL, Icon: dbApp.Icon, + Endpoints: codersdk.OAuth2AppEndpoints{ + Authorization: accessURL.ResolveReference(&url.URL{ + Path: "/login/oauth2/authorize", + }).String(), + Token: accessURL.ResolveReference(&url.URL{ + Path: "/login/oauth2/tokens", + }).String(), + // We do not currently support DeviceAuth. + DeviceAuth: "", + }, } } -func OAuth2ProviderApps(dbApps []database.OAuth2ProviderApp) []codersdk.OAuth2ProviderApp { +func OAuth2ProviderApps(accessURL *url.URL, dbApps []database.OAuth2ProviderApp) []codersdk.OAuth2ProviderApp { apps := []codersdk.OAuth2ProviderApp{} for _, dbApp := range dbApps { - apps = append(apps, OAuth2ProviderApp(dbApp)) + apps = append(apps, OAuth2ProviderApp(accessURL, dbApp)) } return apps } diff --git a/codersdk/oauth2.go b/codersdk/oauth2.go index acebc9c718ac7..318743959d5dc 100644 --- a/codersdk/oauth2.go +++ b/codersdk/oauth2.go @@ -14,6 +14,18 @@ type OAuth2ProviderApp struct { Name string `json:"name"` CallbackURL string `json:"callback_url"` Icon string `json:"icon"` + + // Endpoints are included in the app response for easier discovery. The OAuth2 + // spec does not have a defined place to find these (for comparison, OIDC has + // a '/.well-known/openid-configuration' endpoint). + Endpoints OAuth2AppEndpoints `json:"endpoints"` +} + +type OAuth2AppEndpoints struct { + Authorization string `json:"authorization"` + Token string `json:"token"` + // DeviceAuth is optional. + DeviceAuth string `json:"device_authorization"` } // OAuth2ProviderApps returns the applications configured to authenticate using diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index 956bb75653dca..1ae77d4b7edbb 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -454,6 +454,11 @@ curl -X GET http://coder-server:8080/api/v2/oauth2-provider/apps \ [ { "callback_url": "string", + "endpoints": { + "authorization": "string", + "device_authorization": "string", + "token": "string" + }, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "string" @@ -471,13 +476,17 @@ curl -X GET http://coder-server:8080/api/v2/oauth2-provider/apps \ Status Code **200** -| Name | Type | Required | Restrictions | Description | -| ---------------- | ------------ | -------- | ------------ | ----------- | -| `[array item]` | array | false | | | -| `» callback_url` | string | false | | | -| `» icon` | string | false | | | -| `» id` | string(uuid) | false | | | -| `» name` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------- | -------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[array item]` | array | false | | | +| `» callback_url` | string | false | | | +| `» endpoints` | [codersdk.OAuth2AppEndpoints](schemas.md#codersdkoauth2appendpoints) | false | | Endpoints are included in the app response for easier discovery. The OAuth2 spec does not have a defined place to find these (for comparison, OIDC has a '/.well-known/openid-configuration' endpoint). | +| `»» authorization` | string | false | | | +| `»» device_authorization` | string | false | | Device authorization is optional. | +| `»» token` | string | false | | | +| `» icon` | string | false | | | +| `» id` | string(uuid) | false | | | +| `» name` | string | false | | | To perform this operation, you must be authenticated. [Learn more](authentication.md). @@ -518,6 +527,11 @@ curl -X POST http://coder-server:8080/api/v2/oauth2-provider/apps \ ```json { "callback_url": "string", + "endpoints": { + "authorization": "string", + "device_authorization": "string", + "token": "string" + }, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "string" @@ -558,6 +572,11 @@ curl -X GET http://coder-server:8080/api/v2/oauth2-provider/apps/{app} \ ```json { "callback_url": "string", + "endpoints": { + "authorization": "string", + "device_authorization": "string", + "token": "string" + }, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "string" @@ -610,6 +629,11 @@ curl -X PUT http://coder-server:8080/api/v2/oauth2-provider/apps/{app} \ ```json { "callback_url": "string", + "endpoints": { + "authorization": "string", + "device_authorization": "string", + "token": "string" + }, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "string" diff --git a/docs/api/schemas.md b/docs/api/schemas.md index a51b3bcdfd3df..3ec2e2ede886d 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3519,6 +3519,24 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `id` | string | true | | | | `username` | string | true | | | +## codersdk.OAuth2AppEndpoints + +```json +{ + "authorization": "string", + "device_authorization": "string", + "token": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------------- | ------ | -------- | ------------ | --------------------------------- | +| `authorization` | string | false | | | +| `device_authorization` | string | false | | Device authorization is optional. | +| `token` | string | false | | | + ## codersdk.OAuth2Config ```json @@ -3572,6 +3590,11 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ```json { "callback_url": "string", + "endpoints": { + "authorization": "string", + "device_authorization": "string", + "token": "string" + }, "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "string" @@ -3580,12 +3603,13 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| -------------- | ------ | -------- | ------------ | ----------- | -| `callback_url` | string | false | | | -| `icon` | string | false | | | -| `id` | string | false | | | -| `name` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| -------------- | ---------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `callback_url` | string | false | | | +| `endpoints` | [codersdk.OAuth2AppEndpoints](#codersdkoauth2appendpoints) | false | | Endpoints are included in the app response for easier discovery. The OAuth2 spec does not have a defined place to find these (for comparison, OIDC has a '/.well-known/openid-configuration' endpoint). | +| `icon` | string | false | | | +| `id` | string | false | | | +| `name` | string | false | | | ## codersdk.OAuth2ProviderAppSecret diff --git a/enterprise/coderd/oauth2.go b/enterprise/coderd/oauth2.go index 3d4c822131f1e..3ebb39aaee887 100644 --- a/enterprise/coderd/oauth2.go +++ b/enterprise/coderd/oauth2.go @@ -54,7 +54,7 @@ func (api *API) oAuth2ProviderApps(rw http.ResponseWriter, r *http.Request) { httpapi.InternalServerError(rw, err) return } - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApps(dbApps)) + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApps(api.AccessURL, dbApps)) } // @Summary Get OAuth2 application. @@ -65,10 +65,10 @@ func (api *API) oAuth2ProviderApps(rw http.ResponseWriter, r *http.Request) { // @Param app path string true "App ID" // @Success 200 {object} codersdk.OAuth2ProviderApp // @Router /oauth2-provider/apps/{app} [get] -func (*API) oAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) { +func (api *API) oAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() app := httpmw.OAuth2ProviderApp(r) - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApp(app)) + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApp(api.AccessURL, app)) } // @Summary Create OAuth2 application. @@ -101,7 +101,7 @@ func (api *API) postOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) { }) return } - httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.OAuth2ProviderApp(app)) + httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.OAuth2ProviderApp(api.AccessURL, app)) } // @Summary Update OAuth2 application. @@ -135,7 +135,7 @@ func (api *API) putOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) { }) return } - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApp(app)) + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.OAuth2ProviderApp(api.AccessURL, app)) } // @Summary Delete OAuth2 application. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7127b5f72c114..48d05be9d9e73 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -656,6 +656,13 @@ export interface MinimalUser { readonly avatar_url: string; } +// From codersdk/oauth2.go +export interface OAuth2AppEndpoints { + readonly authorization: string; + readonly token: string; + readonly device_authorization: string; +} + // From codersdk/deployment.go export interface OAuth2Config { readonly github: OAuth2GithubConfig; @@ -678,6 +685,7 @@ export interface OAuth2ProviderApp { readonly name: string; readonly callback_url: string; readonly icon: string; + readonly endpoints: OAuth2AppEndpoints; } // From codersdk/oauth2.go diff --git a/site/src/pages/DeploySettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx b/site/src/pages/DeploySettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx index dc0629e10b61b..2a28ad668d340 100644 --- a/site/src/pages/DeploySettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx +++ b/site/src/pages/DeploySettingsPage/OAuth2AppsSettingsPage/EditOAuth2AppPageView.tsx @@ -1,4 +1,4 @@ -import { useTheme } from "@emotion/react"; +import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import CopyIcon from "@mui/icons-material/FileCopyOutlined"; import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; import Divider from "@mui/material/Divider"; @@ -139,16 +139,28 @@ export const EditOAuth2AppPageView: FC = ({ onCancel={() => setShowDelete(false)} /> -

Client ID

- - {app.id}{" "} - - +
+
Client ID
+
+ + {app.id} + +
+
Authorization URL
+
+ + {app.endpoints.authorization}{" "} + + +
+
Token URL
+
+ + {app.endpoints.token}{" "} + + +
+
@@ -303,3 +315,16 @@ const OAuth2SecretRow: FC = ({ ); }; + +const styles = { + dataList: { + display: "grid", + gridTemplateColumns: "max-content auto", + "& > dt": { + fontWeight: "bold", + }, + "& > dd": { + marginLeft: 10, + }, + }, +} satisfies Record>; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 591536334694c..b4f6db93d9bad 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3371,6 +3371,11 @@ export const MockOAuth2ProviderApps: TypesGen.OAuth2ProviderApp[] = [ name: "foo", callback_url: "http://localhost:3001", icon: "/icon/github.svg", + endpoints: { + authorization: "http://localhost:3001/login/oauth2/authorize", + token: "http://localhost:3001/login/oauth2/token", + device_authorization: "", + }, }, ];