Compare commits

...
Author SHA1 Message Date
sonarly-bot 2f7ca726e4 fix: show actionable error when OAuth redirect_uri is missing
https://sonarly.com/issue/41593?type=bug

The OAuth consent screen allows clicking “Authorize” but performs no action when the URL lacks <code>redirect_uri</code>. This creates a dead-end in an auth/onboarding flow.

Fix: I implemented the root-cause fix in the frontend OAuth authorize flow so users no longer hit a silent dead-end when `redirect_uri` is missing.

What changed:
- In `Authorize.tsx`:
  - Added explicit `isMissingRedirectUrl` validation.
  - Updated `handleAuthorize` to set a clear actionable error message and return early when required params are missing.
  - Rendered that message in the consent UI when `redirect_uri` is absent.
  - Disabled the Authorize button for invalid links to prevent repeated no-op clicks.
- In `AuthorizeActionButtons.tsx`:
  - Added an optional `isAuthorizeDisabled` prop and wired it to the Authorize button `disabled` state.

This preserves existing OAuth behavior (backend still requires redirect URI) while making the frontend fail clearly and safely for malformed auth URLs.

Authored by Sonarly by autonomous analysis (run 47443).
2026-05-29 23:39:50 +00:00
3 changed files with 47 additions and 23 deletions
@@ -7,6 +7,7 @@ type AuthorizeActionButtonsProps = {
onAuthorize: () => void;
onCancel: () => void;
isLoading?: boolean;
isAuthorizeDisabled?: boolean;
};
const StyledButtonContainer = styled.div`
@@ -32,6 +33,7 @@ export const AuthorizeActionButtons = ({
onAuthorize,
onCancel,
isLoading,
isAuthorizeDisabled,
}: AuthorizeActionButtonsProps) => {
const { t } = useLingui();
@@ -47,7 +49,7 @@ export const AuthorizeActionButtons = ({
<StyledAuthorizeButton
title={isLoading ? t`Authorizing...` : t`Authorize`}
onClick={onAuthorize}
disabled={isLoading}
disabled={isLoading || isAuthorizeDisabled}
fullWidth
/>
</StyledButtonContainer>
@@ -152,6 +152,8 @@ export const Authorize = () => {
searchParam.get('redirect_uri') ?? searchParam.get('redirectUrl');
const state = searchParam.get('state');
const isMissingRedirectUrl = !isDefined(redirectUrl);
const {
data,
loading,
@@ -176,28 +178,34 @@ export const Authorize = () => {
}, [shouldRedirectToNotFound, navigate]);
const handleAuthorize = async () => {
if (isDefined(clientId) && isDefined(redirectUrl)) {
setIsAuthorizing(true);
setAuthorizeError(null);
if (!isDefined(clientId) || isMissingRedirectUrl) {
setAuthorizeError(
t`Invalid authorization link: missing redirect URI. Please contact the app developer.`,
);
await authorizeApp({
variables: {
clientId,
codeChallenge: codeChallenge ?? undefined,
redirectUrl,
state: state ?? undefined,
},
onCompleted: (responseData) => {
redirect(responseData.authorizeApp.redirectUrl);
},
onError: (error) => {
setIsAuthorizing(false);
setAuthorizeError(
error.message || t`Authorization failed. Please try again.`,
);
},
});
return;
}
setIsAuthorizing(true);
setAuthorizeError(null);
await authorizeApp({
variables: {
clientId,
codeChallenge: codeChallenge ?? undefined,
redirectUrl,
state: state ?? undefined,
},
onCompleted: (responseData) => {
redirect(responseData.authorizeApp.redirectUrl);
},
onError: (error) => {
setIsAuthorizing(false);
setAuthorizeError(
error.message || t`Authorization failed. Please try again.`,
);
},
});
};
if (isDefined(queryError)) {
@@ -261,13 +269,17 @@ export const Authorize = () => {
</StyledScopeList>
</StyledPermissionSection>
)}
{authorizeError && (
<StyledErrorText>{authorizeError}</StyledErrorText>
{(authorizeError || isMissingRedirectUrl) && (
<StyledErrorText>
{authorizeError ??
t`Invalid authorization link: missing redirect URI. Please contact the app developer.`}
</StyledErrorText>
)}
<AuthorizeActionButtons
onCancel={() => navigate(AppPath.Index)}
onAuthorize={handleAuthorize}
isLoading={isAuthorizing}
isAuthorizeDisabled={isMissingRedirectUrl}
/>
</ModalContent>
</StyledCardWrapper>
@@ -519,6 +519,16 @@ export class AuthService {
}
if (!authorizeAppInput.redirectUrl) {
const analytics = this.auditService.createContext({
workspaceId: workspace.id,
userId: user.id,
});
await analytics.insertWorkspaceEvent('Monitoring', {
eventName: 'oauth.authorize.invalid-request.missing-redirect-uri',
message: `clientId=${clientId}; workspaceId=${workspace.id}; userId=${user.id}`,
});
throw new AuthException(
`redirectUrl not provided for '${clientId}'`,
AuthExceptionCode.FORBIDDEN_EXCEPTION,