Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

callbackURL response being ignored after 302 redirect from auth URL #42

Closed
hallowatcher opened this issue Nov 29, 2022 · 2 comments
Closed

Comments

@hallowatcher
Copy link

hallowatcher commented Nov 29, 2022

Hey! I don't know where to post this, as it could be either an issue with this library, a remix-auth issue, or just the way my OAuth provider is handling the request and I'm completely new to Remix.

I also don't know how to provide reproduction steps, since it might be specific to this provider.

So basically what is happening, is that my callbackURL is being completely ignored, and I'm getting redirected back to the login route. BUT the first login with my provider is working, since it redirects to the provider's website and asks for consent. But once we have consent, the logins after that are "instant" (no consent asked).

But this "instant" functionality is making my callbackURL be skipped entirely.

Here's the logs when no consent is given, and the flow is normal:

GET / 200 - - 12.049 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/login +0ms
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +0ms
  OAuth2Strategy Redirecting to callback URL +2ms
  OAuth2Strategy State 9b7048dd-bbf8-4730-a8b6-d065f40759f0 +0ms
POST /api/auth/login?_data=routes%2Fapi%2Fauth%2Flogin 204 - - 9.372 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/callback?code=def50200470... +3s
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +1ms
  OAuth2Strategy State from URL 9b7048dd-bbf8-4730-a8b6-d065f40759f0 +1ms
  OAuth2Strategy State from session 9b7048dd-bbf8-4730-a8b6-d065f40759f0 +0ms
  OAuth2Strategy State is valid +0ms
Fetching user...
Fetched user hallowatcher
TODO create or fetch user { provider: 'osu', id: '1874761', username: 'hallowatcher' }
  OAuth2Strategy User authenticated +1s
GET /api/auth/callback?code=def502004... 302 - - 1291.926 ms
Homepage User { provider: 'osu', id: '1874761', username: 'hallowatcher' }
GET / 200 - - 9.630 ms

Here's the logs once consent has been given, and the OAuth provider instantly redirects back:

GET / 200 - - 1651.086 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/login +0ms
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +1ms
  OAuth2Strategy Redirecting to callback URL +2ms
  OAuth2Strategy State 49fa4df2-6721-49d3-847a-b4507d1ac04d +0ms
POST /api/auth/login?_data=routes%2Fapi%2Fauth%2Flogin 204 - - 9.126 ms
Error: You made a GET request to http://localhost:4200/api/auth/login but did not provide a default component or `loader` for route "routes/api/auth/login", so there is no way to handle the request.
GET /api/auth/login 500 - - 7.771 ms

And here's the network tab for that:
image

As you can see, the authorize and callback requests have no statuses, so my browser tries to GET the login route which doesn't exist, only POST exists for that. And adding a redirect from the login route also does not help, it simply redirects and the user is not logged in in the end. But it does allow the flow to complete, here's the logs with a redirect from the login page:

GET /?_data=root 200 - - 4.842 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/login +9m
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +1ms
  OAuth2Strategy Redirecting to callback URL +0ms
  OAuth2Strategy State 64d7c39a-7d51-40c7-98d5-9772e64e84e6 +1ms
POST /api/auth/login?_data=routes%2Fapi%2Fauth%2Flogin 204 - - 6.430 ms
  OAuth2Strategy Request URL http://localhost:4200/api/auth/callback?code=def5020027... +547ms
  OAuth2Strategy Callback URL URL {
  href: 'http://localhost:4200/api/auth/callback',
  origin: 'http://localhost:4200',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'localhost:4200',
  hostname: 'localhost',
  port: '4200',
  pathname: '/api/auth/callback',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
} +1ms
  OAuth2Strategy State from URL 64d7c39a-7d51-40c7-98d5-9772e64e84e6 +0ms
  OAuth2Strategy State from session 64d7c39a-7d51-40c7-98d5-9772e64e84e6 +1ms
  OAuth2Strategy State is valid +0ms
GET /api/auth/callback?code=def5020027f3... - - - - ms
GET /api/auth/login 302 - - 4.362 ms
Homepage User null
GET / 200 - - 11.102 ms
Fetching user...
Fetched user hallowatcher
TODO create or fetch user { provider: 'osu', id: '1874761', username: 'hallowatcher' }
  OAuth2Strategy User authenticated +1s

As you can see, the OAuth strategy actually completes in this case, but it is not awaited. The user is already on the page for a second or two before OAuth2Strategy User authenticated is shown.


Here's my business logic for reference:

app/src/Layout.tsx

...
<Form action="/api/auth/login" method="post">
  <Button type="submit">
   Log in
  </Button>
</Form>
...

app/routes/api/auth/login.ts

// export let loader: LoaderFunction = () => redirect('/');

export const action: ActionFunction = ({ request }) => {
  return authenticator.authenticate('osu', request);
}

app/routes/api/auth/callback.ts

export const loader: LoaderFunction = ({ request }) => {
  return authenticator.authenticate('osu', request, {
    successRedirect: '/',
    failureRedirect: '/'
  });
}

app/services/osu.strategy.ts

export class OsuStrategy<User> extends OAuth2Strategy<User, OsuProfile, OsuExtraParams> {
  public readonly name = 'osu';

  private readonly scope: string;
  private readonly userInfoURL: string;

  // We receive our custom options and our verify callback
  constructor(
    options: OsuStrategyOptions,
    verify: StrategyVerifyCallback<User, OAuth2StrategyVerifyParams<OsuProfile, OsuExtraParams>>
  ) {
    const configuration: OAuth2StrategyOptions = {
      authorizationURL: `https://osu.ppy.sh/oauth/authorize`,
      tokenURL: `https://osu.ppy.sh/oauth/token`,
      clientID: options.clientID,
      clientSecret: options.clientSecret,
      callbackURL: options.callbackURL,
      responseType: 'code'
    };

    super(configuration, verify);

    this.userInfoURL = `https://osu.ppy.sh/api/v2/me/osu`;
    this.scope = options.scope || 'public';
  }

  protected authorizationParams() {
    const urlSearchParams: Record<string, string> = {
      scope: this.scope,
    };

    return new URLSearchParams(urlSearchParams);
  }

  protected async userProfile(accessToken: string): Promise<OsuProfile> {
    console.log('Fetching user...');
    let response = await fetch(this.userInfoURL, {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    let data: ApiUser = await response.json();

    console.log('Fetched user', data.username);
    return {
      provider: this.name,
      id: String(data.id),
      username: data.username,
    } as OsuProfile;
  }
}

app/services/osu.strategy.ts

export let authenticator = new Authenticator<unknown>(sessionStorage);

const osuStrategy = new OsuStrategy(
  {
    clientID: '...',
    clientSecret: '...',
    callbackURL: 'http://localhost:4200/api/auth/callback',
    scope: 'friends.read'
  },
  async ({ profile }) => {

    console.log('TODO create or fetch user', profile);

    return profile;
  }
);

authenticator.use(osuStrategy);

app/services/session.server.ts

export let sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '_session',
    sameSite: 'lax',
    path: '/',
    httpOnly: true,
    secrets: ['s3cr3t'], // TODO replace with actual secret
    secure: process.env.NODE_ENV === 'production',
  },
});

// you can also export the methods individually for your own usage
export let { getSession, commitSession, destroySession } = sessionStorage;

I know this is a lot to look at, and I'm not expecting much. I'm currently checking out Remix and seeing if it fits my needs in order to change from Angular + NestJS. Thanks!

@hallowatcher
Copy link
Author

After a bit further investigation and debugging, I found that the problem lies in the fetchAccessToken function. Once the fetch gets triggered, for some reason we receive a redirect on the browser, which gets followed even though the endpoint hasn't finished running.

NbyQRIbl.mp4

This would be here: https://github.com/sergiodxa/remix-auth-oauth2/blob/main/src/index.ts#L356

Now I'm wondering how I can get around this, since it seems like it is specific to the way OAuth is implemented for this case (instant redirect).

@hallowatcher hallowatcher changed the title callbackURL being skipped callbackURL response being ignored after 302 redirect from auth URL Nov 29, 2022
@hallowatcher
Copy link
Author

I figured it out! (sorry for spamming my own issue)

After days of debugging and digging around, here is the fix: remix-run/remix#2997 (comment)

So this is definitely not an issue on this side. This can be closed 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants