Repository: hashicorp/vault
File: ui/app/routes/vault/cluster/oidc-provider.js (JavaScript)

Code before:
      `${this.win.origin}/v1/identity/oidc/provider/${routeParams.provider_name}/authorize`,
      qp
    );
    if (!qp.redirect_uri) {
      throw new Error('Missing required query params');
    }
    try {
      const response = await this.auth.ajax(endpoint, 'GET', { namespace: routeParams.namespace });
      if ('consent' === qp.prompt?.toLowerCase()) {
        return {
          consent: {
            code: response.code,
            redirect: decodedRedirect,
            state: qp.state,
          },
        };
      }
      return this._handleSuccess(response, decodedRedirect, qp.state);
    } catch (errorRes) {
      const resp = await errorRes.json();
      const code = resp.error;
      const permissionDenied = resp?.errors?.some((str) => str.includes('permission denied'));
      if (code === 'max_age_violation' || permissionDenied) {
        this._redirectToAuth({ ...routeParams, qp, logout: true });
      } else if (code === 'invalid_redirect_uri') {
        return {
          error: {
            title: 'Redirect URI mismatch',
            message:
              'The provided redirect_uri is not in the list of allowed redirect URIs. Please make sure you are sending a valid redirect URI from your application.',
          },
        };
      } else if (code === 'invalid_client_id') {
        return {
          error: {
            title: 'Invalid client ID',
            message: 'Your client ID is invalid. Please update your configuration and try again.',
          },
        };
      } else {
        return this._handleError(resp, decodedRedirect);
      }
    }
  }
}


Review comment (security):
Maybe add a comment here to clarify it's important we check a partial match and not an exact match?
```suggestion
// We're using a partial match ("includes") to catch any occurrence of "permission denied" in error messages.
// This ensures we catch permissions errors in all workflows, even if the message is not an exact match.
      const permissionDenied = resp?.errors?.some((str) => str.includes('permission denied'));
```

Code after:
      `${this.win.origin}/v1/identity/oidc/provider/${routeParams.provider_name}/authorize`,
      qp
    );
    if (!qp.redirect_uri) {
      throw new Error('Missing required query params');
    }
    try {
      const response = await this.auth.ajax(endpoint, 'GET', { namespace: routeParams.namespace });
      if ('consent' === qp.prompt?.toLowerCase()) {
        return {
          consent: {
            code: response.code,
            redirect: decodedRedirect,
            state: qp.state,
          },
        };
      }
      return this._handleSuccess(response, decodedRedirect, qp.state);
    } catch (errorRes) {
      const resp = await errorRes.json();
      const code = resp.error;
      // This go-multierror package formats multiple errors as a single string:
      // https://github.com/hashicorp/go-multierror/blob/main/format.go#L28
      // Example: '2 errors occurred:\n\t* permission denied\n\t* invalid token\n\n'
      // So check for substrings of "permission denied" within each full error message.
      const permissionDenied = resp?.errors?.some((str) => str.includes('permission denied'));
      if (code === 'max_age_violation' || permissionDenied) {
        this._redirectToAuth({ ...routeParams, qp, logout: true });
      } else if (code === 'invalid_redirect_uri') {
        return {
          error: {
            title: 'Redirect URI mismatch',
            message:
              'The provided redirect_uri is not in the list of allowed redirect URIs. Please make sure you are sending a valid redirect URI from your application.',
          },
        };
      } else if (code === 'invalid_client_id') {
        return {
          error: {
            title: 'Invalid client ID',
            message: 'Your client ID is invalid. Please update your configuration and try again.',
          },
        };
      } else {
        return this._handleError(resp, decodedRedirect);
      }
    }
  }
}