Skip to content

Commit 3a25e91

Browse files
authored
feat(oauth): Show public app device flow URLs (#111655)
Show the device authorization and verification URLs on the OAuth application details page for public clients. Public clients can use device flow today, but the settings UI only exposes the authorize and token endpoints. This adds the static device-flow URLs to the same credentials block so CLI and native app setups do not need to bounce back to the auth docs. This keeps the extra fields scoped to public apps and normalizes `urlPrefix` before composing the OAuth URLs so copied values do not contain duplicate slashes.
1 parent ae135a8 commit 3a25e91

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

static/app/views/settings/account/apiApplications/details.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
import ApiApplicationDetails from 'sentry/views/settings/account/apiApplications/details';
99

1010
describe('ApiApplicationDetails', () => {
11+
const oauthBaseUrl = 'https://sentry-jest-tests.example.com/oauth';
12+
1113
it('renders basic details for confidential client', async () => {
1214
MockApiClient.addMockResponse({
1315
url: '/api-applications/abcd/',
@@ -55,6 +57,10 @@ describe('ApiApplicationDetails', () => {
5557
expect(screen.getByLabelText('Client Secret')).toBeInTheDocument();
5658
expect(screen.getByLabelText('Authorization URL')).toBeInTheDocument();
5759
expect(screen.getByLabelText('Token URL')).toBeInTheDocument();
60+
expect(screen.getByDisplayValue(`${oauthBaseUrl}/authorize/`)).toBeInTheDocument();
61+
expect(screen.getByDisplayValue(`${oauthBaseUrl}/token/`)).toBeInTheDocument();
62+
expect(screen.queryByLabelText('Device Authorization URL')).not.toBeInTheDocument();
63+
expect(screen.queryByLabelText('Device Verification URL')).not.toBeInTheDocument();
5864
});
5965

6066
it('handles client secret rotation', async () => {
@@ -160,6 +166,10 @@ describe('ApiApplicationDetails', () => {
160166
expect(screen.getByLabelText('Client ID')).toBeInTheDocument();
161167
expect(screen.getByDisplayValue('public-app')).toBeInTheDocument();
162168
expect(screen.getByDisplayValue('Public CLI App')).toBeInTheDocument();
169+
expect(screen.getByDisplayValue(`${oauthBaseUrl}/authorize/`)).toBeInTheDocument();
170+
expect(screen.getByDisplayValue(`${oauthBaseUrl}/token/`)).toBeInTheDocument();
171+
expect(screen.getByDisplayValue(`${oauthBaseUrl}/device/code/`)).toBeInTheDocument();
172+
expect(screen.getByDisplayValue(`${oauthBaseUrl}/device/`)).toBeInTheDocument();
163173
});
164174

165175
it('renders confidential client with client secret section', async () => {
@@ -202,5 +212,7 @@ describe('ApiApplicationDetails', () => {
202212
expect(
203213
screen.queryByText(/This is a public client, designed for CLIs/)
204214
).not.toBeInTheDocument();
215+
expect(screen.queryByLabelText('Device Authorization URL')).not.toBeInTheDocument();
216+
expect(screen.queryByLabelText('Device Verification URL')).not.toBeInTheDocument();
205217
});
206218
});

static/app/views/settings/account/apiApplications/details.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Fragment} from 'react';
22
import styled from '@emotion/styled';
3+
import trimEnd from 'lodash/trimEnd';
34

45
import {Alert} from '@sentry/scraps/alert';
56
import {Tag} from '@sentry/scraps/badge';
@@ -54,6 +55,7 @@ function ApiApplicationsDetails() {
5455
const queryClient = useQueryClient();
5556

5657
const urlPrefix = ConfigStore.get('urlPrefix');
58+
const oauthBaseUrl = `${trimEnd(urlPrefix, '/')}/oauth`;
5759

5860
const {
5961
data: app,
@@ -173,12 +175,27 @@ function ApiApplicationsDetails() {
173175
)}
174176

175177
<FieldGroup label={t('Authorization URL')} flexibleControlStateSize>
176-
<TextCopyInput>{`${urlPrefix}/oauth/authorize/`}</TextCopyInput>
178+
<TextCopyInput>{`${oauthBaseUrl}/authorize/`}</TextCopyInput>
177179
</FieldGroup>
178180

179181
<FieldGroup label={t('Token URL')} flexibleControlStateSize>
180-
<TextCopyInput>{`${urlPrefix}/oauth/token/`}</TextCopyInput>
182+
<TextCopyInput>{`${oauthBaseUrl}/token/`}</TextCopyInput>
181183
</FieldGroup>
184+
185+
{app.isPublic && (
186+
<Fragment>
187+
<FieldGroup
188+
label={t('Device Authorization URL')}
189+
flexibleControlStateSize
190+
>
191+
<TextCopyInput>{`${oauthBaseUrl}/device/code/`}</TextCopyInput>
192+
</FieldGroup>
193+
194+
<FieldGroup label={t('Device Verification URL')} flexibleControlStateSize>
195+
<TextCopyInput>{`${oauthBaseUrl}/device/`}</TextCopyInput>
196+
</FieldGroup>
197+
</Fragment>
198+
)}
182199
</PanelBody>
183200
</Panel>
184201
</Form>

0 commit comments

Comments
 (0)