hydra icon indicating copy to clipboard operation
hydra copied to clipboard

Difference in behavior when user denied consent for HTTPS and non-HTTPS redirect URI

Open viralsavaniIM opened this issue 1 year ago • 0 comments

Preflight checklist

Describe the bug

I am implementing my own User Login and Consent flow for ORY Hydra. I'm noticing a difference in redirect behavior when a user denies consent.

When the auth is invoked from the OAuth client in the mobile app i.e. using non-HTTPS redirect URI, Hydra's error endpoint is invoked (one set in the config YAML)

URL on completion of deny consent: https://account.acme.localhost/error?error=consent_denied&error_description=Consent+denied.+Please+try+again+and+consent+the+data+sharing

But when the same OAuth Client in the Web app with HTTPS redirect URI initiates the flow, it actually calls the Web app's redirect URI.

URL on completion of deny consent: https://service.acme.localhost/callback?error=consent_denied&error_description=Consent+denied.+Please+try+again+and+consent+the+data+sharing

Describe your ideal solution

HYDRA should call localhost.acme.service://callback?error=consent_denied&error_description=Consent+denied.+Please+try+again+and+consent+the+data+sharing for the non-HTTPS redirect URIs.

Reproducing the bug

  1. Start with 5min-tutorial
  2. Add the OAuth Client with details as mentioned in the Additional Context section
  3. Create a simple web app to test OAuth workflow.
  4. Create a simple iOS app to test the OAuth workflow
  5. Perform the OAuth on both apps and press deny consent.

Relevant log output

No response

Relevant configuration

urls:
  self:
    issuer: https://account.acme.localhost
  consent: https://account.acme.localhost/consent
  login: https://account.acme.localhost/login
  logout: https://account.acme.localhost/logout
  error: https://account.acme.localhost/error

Version

v1.11.9

On which operating system are you observing this issue?

Other

In which environment are you deploying?

Docker Compose

Additional Context

Here is the OAuth Client information

{
    "client_id": "TestOAuth2Client",
    "client_name": "Test OAuth2Client",
    "redirect_uris": [
        "localhost.acme.service://callback",
        "https://service.acme.localhost/callback"
    ],
    "grant_types": [
        "authorization_code"
    ],
    "response_types": [
        "code"
    ],
    "scope": "openid email profile offline offline_access",
    "audience": [],
    "owner": "",
    "policy_uri": "***",
    "allowed_cors_origins": [],
    "tos_uri": "***",
    "client_uri": "***",
    "logo_uri": "***",
    "contacts": [],
    "client_secret_expires_at": 0,
    "subject_type": "public",
    "jwks": {},
    "token_endpoint_auth_method": "client_secret_basic",
    "userinfo_signed_response_alg": "none",
    "created_at": "***",
    "updated_at": "***",
    "metadata": {},
    "authorization_code_grant_access_token_lifespan": null,
    "authorization_code_grant_id_token_lifespan": null,
    "authorization_code_grant_refresh_token_lifespan": null,
    "client_credentials_grant_access_token_lifespan": null,
    "implicit_grant_access_token_lifespan": null,
    "implicit_grant_id_token_lifespan": null,
    "jwt_bearer_grant_access_token_lifespan": null,
    "password_grant_access_token_lifespan": null,
    "password_grant_refresh_token_lifespan": null,
    "refresh_token_grant_id_token_lifespan": null,
    "refresh_token_grant_access_token_lifespan": null,
    "refresh_token_grant_refresh_token_lifespan": null
}

Here is the NodeJS code which handles the user consent granted/deny

if (req.body.consentGranted) {
  // get the consent request first so it that fails no need to query user details in generateConsentSession
  const consentRequest = await Hydra.getConsentRequest(challenge);
  const session = {
    access_token: {},
    id_token: {...},
  };
  const acceptConsent = await Hydra.acceptConsentRequest(challenge, {
    grant_scope: grantScope,
    grant_access_token_audience:
      consentRequest.data.requested_access_token_audience,
    remember: true,
    remember_for: 0,
    session,
  });
  return res.status(acceptConsent.status).send({ redirect: accept.data.redirect_to });
}

const denyConsent = await Hydra.rejectConsentRequest(challenge, {
  error: 'consent_denied',
  error_description: 'Consent denied.',
  error_debug: JSON.stringify({ grantScope, challenge }, null, 2),
  error_hint: 'Please try again and consent the data sharing.',
});
return res.status(denyConsent.status).send({
  error: 'Consent denied. Please try again and consent the data sharing',
  redirect: denyConsent.data.redirect_to,
});

Here is output of the denyConsent.data.redirect_to

  • For the web app:
https://account.acme.localhost/oauth2/auth?client_id=TestOAuth2Client
  & code_challenge=***
  & code_challenge_method=S256
  & consent_verifier=***
  & nonce=***
  & redirect_uri=https%3A%2F%2Fservice.acme.localhost%2Fcallback
  & response_type=code
  & scope=openid+email+profile+offline+offline_access
  & state=***
  • For the mobile app:
https://account.acme.localhost/oauth2/auth?client_id=TestOAuth2Client
  & code_challenge=***
  & code_challenge_method=S256
  & consent_verifier=***
  & nonce=***
  & redirect_uri=localhost.acme.service%3A%2F%2Fcallback
  & response_type=code
  & scope=openid+profile+email+offline_access+offline
  & state=***

viralsavaniIM avatar May 10 '23 21:05 viralsavaniIM