ReactJS sent GET request, responded with 302 redirect, received CORS error
Asked Answered
A

3

7

This is my stack:

  1. Frontend server: Node + ReactJS (f.com)
  2. Backend server (API): Golang (b.com)

I'm doing this in development environment where I use create-react-app to run my frontend server via npm start command. I want to perform authentication using SSO served at sso.example.com/login. Ideally whenever there's a request to the Backend server without proper authorization, it will be responded with HTTP 302 Redirect to the SSO page. So the sequence is:

  1. ReactJS sends GET request to b.com/users
  2. Golang responded with http.Redirect("sso.example.com/login")
  3. ReactJS app gets CORS error from sso.example.com of which I don't have control of adding Access-Control-Allow-Origin:* respond header

How can I redirect my GET request successfully?

Aspire answered 1/10, 2017 at 16:40 Comment(3)
Does your react app get the cors error response from b.com or from sso.example.com?Hectoliter
@Hectoliter oh yeah I just added that into the question. it's from sso.example.comAspire
If sso.example.com does not allow cross origin requests and you don't have access to its server to modify the access controll then I'm not sure your react app will ever be able to access its resources.Hectoliter
G
4

In your case, it would be easier to do the redirection in browser on the client side, so you won't have CORS issues (because you are accessing it from the SSO website url directly).

You can either do it with react, which is complicated: Programmatically navigate using react router V4

Or do it with window with plain JavaScript, which is easy but might cause problems with browsing history:

window.location.href = "https://www.example.com";

https://appendto.com/2016/04/javascript-redirect-how-to-redirect-a-web-page-with-javascript/

Grundyism answered 1/10, 2017 at 17:22 Comment(1)
ahh okay, that's what I thought. I was just thinking if there's anything that I can do to avoid CORS on redirect in this scenario. thanks for the advice tho!Aspire
B
2

The GET http://b.com/users request sent by React is an XMLHttpRequest or a fetch request, right? Unlike navigation requests, these do not happen in a browsing context. (The Javascript program may receive an HTML page in the response to such a request, but it does nothing with that HTML.) As a consequence, these requests cannot be authenticated with an SSO logon flow, because SSO protocols like SAML or OpenID Connect rely on a browsing context where the browser renders the HTML page that comes with a response. (And if the user is not yet logged on, they have to type their credentials into the HTML page.)

If your React app relies on requests to b.com, then you should perform in a visible browsing context an SSO logon flow to that site before your app starts making any requests to it. You could display a button "Log on to b.com" which opens a popup at b.com. Inside this popup, the browser is then redirected to sso.example.com/login and (perhaps after the user gave their credentials) back to b.com. At that point, the user is logged on to b.com and can close the popup again. (There are also mechanisms to auto-close such a login popup.)

All this would happen before the React app makes any fetch requests to b.com, and since it happens in a browsing context, no CORS would be involved at all.

(See also Access to fetch at https://accounts.google.com/o/oauth2/v2/auth has been blocked by CORS.)

What changes with third-party cookie blocking?

The answer given above assumes that a fetch request to b.com contains the "session cookie" from b.com that was set during the SSO logon flow. However, if the React app runs on f.com, this will no longer be true once browsers start blocking third-party cookies ("3PCD").

A 3PCD-proof solution would be to execute the SSO logon flow in an iframe instead of a popup and have b.com set a partitioned session cookie (with partition key https://f.com). But since the session cookie from sso.example.com would also count as third-party, this would not work either, unless sso.example.com uses the Storage Access API to access its session cookie even while embedded in f.com.

Beggarly answered 16/12, 2023 at 10:47 Comment(0)
K
0

@paradite answer covers your question fully, I would add that there are React libraries that handles that for you in the frontend: https://www.npmjs.com/package/@react-oauth/google or https://www.npmjs.com/package/react-social-login-buttons

It allows you to use a pre-made button, as simple as (in case of the first package)

<GoogleLogin
  onSuccess={(resp) => { .. call your backend with resp .. })}
  onError={(e) => { ... })}
/>

It really depends on your use-case; there are couple examples in the npm package site, and I used it lately in one of my projects and can say it simplifies the process.

On the backend, you need to decide which flow you gonna use: Signin (simpler) or Authorization (if you need extra scopes, access-tokens, refresh tokens, etc.)

The first is simpler since you get JWT which you decode and validate, and you have the user info.

If you want to implement the latter, have your backend get from the client the code and your backend has to exchange that with the provider, something like this:

type UserData struct {
    Email string
    Name  string
    Token *oauth2.Token
}


var googleOauthConfig = &oauth2.Config{
    RedirectURL:  "http://localhost:1323/oauth/callback",
    ClientID:     "your-client-id.apps.googleusercontent.com",
    ClientSecret: "your-secret",
    Scopes:       []string{"profile", "email"},
    Endpoint:     google.Endpoint,
}

func GetUserDataFromGoogle(code string) (UserData, error) {
    // Use code to get token and get user info from Google.
    var userData UserData
    token, err := googleOauthConfig.Exchange(context.Background(), code)
    if err != nil {
        return userData, fmt.Errorf("code exchange wrong: %s", err.Error())
    }
    response, err := http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token.AccessToken)
    if err != nil {
        return userData, fmt.Errorf("failed getting user info: %s", err.Error())
    }
    defer response.Body.Close()

    contents, err := io.ReadAll(response.Body)
    if err != nil {
        return userData, fmt.Errorf("failed read response: %s", err.Error())
    }
    err = json.Unmarshal(contents, &userData)
    userData.Token = token
    return userData, nil
}

The token includes access_token, refresh_token which you can use to fetch user information from the provider.

Note I'm using https://github.com/golang/oauth2 library here for the protocol implementation. In your handler you extract the code and call this function, if the code is valid you'll get the userData, otherwise you'll get an error.

This bypass the CORS issue, and and simplify the solution. I used Google in the example, but Oauth2 library supports many other providers.

Kisung answered 19/12, 2023 at 9:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.