Google OAuth2 Login redirect uri mismatch on Heroku but not localhost - stuck on a call to backend - reactjs

I am using the MERN stack on a site where I use JWT for register/login. I've set up Google login using react-google-login on my site. On the localhost, everything works fine.
When I deployed to Heroku, I initially got zero response from pressing the button and an error of a redirect-URI-mismatch. So I went into my google developer account and added the Heroku page to the authorized javascript origins.
Currently, the Google login opens and everything appears successful....at first. The initial response (the first logged 'response' shown below) to Google shows up. So I'm getting a code returned.
function GoogleOAuth({ setSignedIn }) {
const responseGoogle = (response) => {
console.log('response', response);
const { code } = response;
if (code) {
axios
.post("/api/users/create-tokens", { code })
.then((res) => {
console.log(res.data);
toast.success("Success! You're connected to Google!")
})
.catch((err) => console.log(err.message));
}
};
In the associated google account, it shows that this app now has access. The problem arises at the second part - where it says if(code). Again, the call to the backend works fine locally but in Heroku it appears 'stuck' here and then the message in the console is redirect uri mismatch. I tried putting in the backend api address to the developer console's redirect uri section but that didn't work. Anyone have any idea what is going on at this point in Heroku? I really can't figure it out.
<GoogleContainer>
<Logo src={GoogleLogo} />
<GoogleLogin
clientId= {CLIENT_ID}
render={(renderProps) => (
<GoogleBtn
onClick={renderProps.onClick}
disabled={renderProps.disabled}
style={styleObj}
>
Connect to Google
</GoogleBtn>
)}
onSuccess={responseGoogle}
isSignedIn={true}
onFailure={responseError}
cookiePolicy={"single_host_origin"}
responseType='code'
accessType='offline'
scope='openid email profile https://www.googleapis.com/auth/calendar'
/>{" "}
</GoogleContainer>
);

Related

Electron App with Azure AD - without Interactive browser

I am trying to integrate Azure AD authentication with my Electron App (with Angular). I took reference from this link and able to integrate: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-nodejs-desktop
Issue: It's using getTokenInteractive() method and it's navigating to an external browser. As per my requirement we don't have to navigate to an external browser, it should open the UI inside my electron App where end users can provide their credentials.
Another option if possible we can open the Azure AD url part of my electron App.
I took reference from this link and able to integrate: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-nodejs-desktop
async getTokenInteractive(tokenRequest) {
try {
const openBrowser = async (url) => {
await shell.openExternal(url);
};
const authResponse = await this.clientApplication.acquireTokenInteractive({
...tokenRequest,
openBrowser,
successTemplate: '<h1>Successfully signed in!</h1> <p>You can close this window now.</p>',
errorTemplate: '<h1>Oops! Something went wrong</h1> <p>Check the console for more information.</p>',
});
return authResponse;
} catch (error) {
throw error;
}
}

Getting unauthorized access error when implementing SAML 2.0 based applications

I am getting this error when clicking on Login with Microsoft button
We're unable to complete your request
unauthorized_client: The client does not exist or is not enabled for consumers.
If you are the application developer, configure a new application through the App Registrations in the Azure Portal at https://go.microsoft.com/fwlink/?linkid=2083908.
I am working on Front-End and I am also passing the correct client-id but still getting this error
Here is the code --
const App = () => {
const loginHandler = (err, data, msal) => {
console.log(err, data);
// some actions
if (!err && data) {
// onMsalInstanceChange(msal);
console.log(msal);
}
};
return (
<div className="app">
<MicrosoftLogin clientId={config.clientId} authCallback={loginHandler} />
</div>
);
};
It looks like you are using OAuth2 / OpenID Connect to login with an application that uses SAML?
You need to create an enterprise application.

How do you "Sign In with Google" with NextJS and Firebase Auth on Safari

I'm using:
firebase: ^8.6.8
next: ^11.0.1
react: ^17.0.2
When trying to sign in with Google on Safari, I get errors related to a Content Blocker with both signInWithPopUp and signInWithRedirect:
Content blocker prevented frame displaying https://my-site.com/page
from loading a resource from https://apis.google.com/js/api.js?onload=__iframefcb318667
Does anybody know how to handle this properly?
Thank you!
Okay, here is the solution I used:
Instead of using signInWithRedirect or signInWithPopup that seem to cause problem on Apple devices using Safari, I directly use [https://apis.google.com/js/platform.js] and then signInWithCredential using the googleUser.
I also used the package react-google-signin [https://www.npmjs.com/package/react-google-login] to simplify my code.
Step by step:
Enable Google Sign-in method on Firebase Console for Authentication
On the Credentials page of Google Cloud Platform [https://console.developers.google.com/apis/credentials], choose your project and under OAuth 2.0 Client IDs get the Client Id.
Save the Client ID as an environment variable NEXT_PUBLIC_GOOGLE_CLIENT_ID in your Project Settings on Vercel.
In your _document.tsx add the https://apis.google.com/js/platform.js script and needed meta data:
<meta name="google-signin-client_id" content={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}></meta>
<meta name="google-signin-cookiepolicy" content="single_host_origin"></meta>
<meta name="google-signin-scope" content="profile email"></meta>
<script src="https://apis.google.com/js/platform.js" async defer></script>
Define your Google Sign in button:
const GoogleButton: FC<GoogleButtonProps> = ({onSuccess, onFailure}) => {
const onSignInFailure = (failure: any) => {
console.error('FAILURE: ', failure);
onFailure(...);
};
const onSignIn = (googleUser: any) => {
signInWithGoogleUser(googleUser)
.then(onSuccess)
.catch(onFailure);
}
return (
<GoogleLogin
render={(renderProps) => (
<button
onClick={renderProps.onClick}
disabled={renderProps.disabled}>
This is my custom Google button
</button>
)}
clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}
buttonText="Login"
onSuccess={onSignIn}
onFailure={onSignInFailed}
cookiePolicy={'single_host_origin'}
prompt="select_account"
/>
);
};
I specified prompt="select_account" to be sure it prompt the user for the account selection even if hse is already logged in.
Then, inside signInWithGoogleUser, is use signInWithCredential to use the googleUser to authentify on Firebase Auth:
export const signInWithGoogleUser = (googleUser: any) => {
const auth = firebase.auth();
const provider = firebase.auth.GoogleAuthProvider;
const credential = provider.credential(googleUser.getAuthResponse().id_token);
return auth.signInWithCredential(credential);
};
NOTES
a. My project is running on Vercel [https://vercel.com/] with the Database (Firestore) and Authentication on Firebase [https://firebase.com]. You will have to adapt my solution if you use another stack.
b. With this, it works on almost all iPhones and Mac computers. In case it still fails, it is due to an installed content/cookie blocker and I display an error message asking the visitor to reactivate cookies or register using email and password.
c. I simplified a bit the code to have a more condensed answer.

Cannot get Cypress test to work in React with Okta

I am trying to test my React (v16.10.2) application with Cypress (v4.5.0). Our application is using Okta for authentication (with the client #okta/okta-react 1.3.1).
I can use my app from the browser without any issues. The first time I login, I get the Okta login screen. I enter my user ID and password, and the react client calls the authn endpoint with my creds, and then calls the authorization endpoint to get the token. I am then taken to the first screen of our application.
When I try to login in my Cypress test, my user ID and password are entered into the login screen, the authn endpoint is called successfully, but the authorization endpoint returns a 403 error. Unfortunately, there is no other info about why I am getting the 403.
I have compared the authorization requests between the one that works in the browser, and the one that doesn't work from Cypress. The only real difference I see is that the working browser request has an origin header, whereas the failing one does not.
Question #1: Could the missing origin header be the cause of my problem?
In order to avoid a bunch of CORS and cross-site issues, I had to install a couple Chrome extensions (ignore-x-frame-headers and Access-Control-Allow-Origin-master). I am implementing them in the following code in cypress/plugins/index.js:
module.exports = (on, config) => {
on('before:browser:launch', (browser = {}, launchOptions) => {
// The following code comes from https://medium.com/#you54f/configuring-cypress-to-work-with-iframes-cross-origin-sites-afff5efcf61f
// We were getting cross-origin errors when trying to run the tests.
if (browser.name === 'chrome') {
const ignoreXFrameHeadersExtension = path.join(__dirname, '../extensions/ignore-x-frame-headers');
launchOptions.args.push(`--load-extension=${ignoreXFrameHeadersExtension}`);
const accessControlAllowOriginMasterExtension = path.join(__dirname, '../extensions/Access-Control-Allow-Origin-master');
launchOptions.args.push(`--load-extension=${accessControlAllowOriginMasterExtension}`);
launchOptions.args.push("--disable-features=CrossSiteDocumentBlockingIfIsolating,CrossSiteDocumentBlockingAlways,IsolateOrigins,site-per-process");
launchOptions.args.push('--disable-site-isolation-trials');
launchOptions.args.push('--reduce-security-for-testing');
launchOptions.args.push('--out-of-blink-cors');
}
if (browser.name === 'electron') {
launchOptions.preferences.webPreferences.webSecurity = false;
}
return launchOptions;
});
I also added the following to cypress.json:
{
"chromeWebSecurity": false
}
Here is my cypress test:
describe('Order Lookup Test', () => {
const UI_URL: string = 'http://localhost:3000/';
const ORDER_NUMBER: string = '10307906234';
beforeEach(() => {
Cypress.config('requestTimeout', 50000);
cy.visit(UI_URL);
cy.get('#okta-signin-username', {timeout: 10000}).type('xxxxxxxx');
cy.get('#okta-signin-password', {timeout: 10000}).type('xxxxxxxx');
cy.get('#okta-signin-submit', {timeout: 10000}).click();
})
it('should return an order', () => {
cy.get('.number-input', {timeout: 10000}).type(ORDER_NUMBER);
cy.get('.order-lookup-buttons-search-valid').should('be.visible').click();
})
})
Does anyone have any idea what might be going on? What other information should I be including in order to help narrow this down?

Authentication with oidc-client.js and Identityserver4 in a React frontend

Lately I'm trying to set-up authentication using IdentityServer4 with a React client. I followed the Adding a JavaScript client tutorial (partly) of the IdentityServer documentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf also using the Quickstart7_JavaScriptClient file.
The downside is that I'm using React as my front-end and my knowledge of React is not good enough to implement the same functionality used in the tutorial using React.
Nevertheless, I start reading up and tried to get started with it anyway. My IdentityServer project and API are set-up and seem to be working correctly (also tested with other clients).
I started by adding the oidc-client.js to my Visual Code project. Next I created a page which get's rendered at the start (named it Authentication.js) and this is the place where the Login, Call API and Logout buttons are included. This page (Authentication.js) looks as follows:
import React, { Component } from 'react';
import {login, logout, api, log} from '../../testoidc'
import {Route, Link} from 'react-router';
export default class Authentication extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div>
<button id="login" onClick={() => {login()}}>Login</button>
<button id="api" onClick={() => {api()}}>Call API</button>
<button id="logout" onClick={() => {logout()}}>Logout</button>
<pre id="results"></pre>
</div>
<div>
<Route exact path="/callback" render={() => {window.location.href="callback.html"}} />
{/* {<Route path='/callback' component={callback}>callback</Route>} */}
</div>
</div>
);
}
}
In the testoidc.js file (which get's imported above) I added all the oidc functions which are used (app.js in the example projects). The route part should make the callback.html available, I have left that file as is (which is probably wrong).
The testoidc.js file contains the functions as follow:
import Oidc from 'oidc-client'
export function log() {
document.getElementById('results').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + '\r\n';
});
}
var config = {
authority: "http://localhost:5000",
client_id: "js",
redirect_uri: "http://localhost:3000/callback.html",
response_type: "id_token token",
scope:"openid profile api1",
post_logout_redirect_uri : "http://localhost:3000/index.html",
};
var mgr = new Oidc.UserManager(config);
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
}
else {
log("User not logged in");
}
});
export function login() {
mgr.signinRedirect();
}
export function api() {
mgr.getUser().then(function (user) {
var url = "http://localhost:5001/identity";
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, JSON.parse(xhr.responseText));
}
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
export function logout() {
mgr.signoutRedirect();
}
There are multiple things going wrong. When I click the login button, I get redirected to the login page of the identityServer (which is good). When I log in with valid credentials I'm getting redirected to my React app: http://localhost:3000/callback.html#id_token=Token
This client in the Identity project is defined as follows:
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
// where to redirect to after login
RedirectUris = { "http://localhost:3000/callback.html" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:3000/index.html" },
AllowedCorsOrigins = { "http://localhost:3000" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
Though, it seems the callback function is never called, it just stays on the callback url with a very long token behind it..
Also the getUser function keeps displaying 'User not logged in' after logging in and the Call API button keeps saying that there is no token. So obviously things are not working correctly. I just don't know on which points it goes wrong.
When inspecting I can see there is a token generated in the local storage:
Also when I click the logout button, I get redirected to the logout page of the Identity Host, but when I click logout there I don't get redirected to my client.
My questions are:
Am I on the right track implementing the oidc-client in combination with IdentityServer4?
Am I using the correct libraries or does react require different libraries than the oidc-client.js one.
Is there any tutorial where a react front-end is used in combination with IdentityServer4 and the oidc-client (without redux), I couldn't find any.
How / where to add the callback.html, should it be rewritten?
Could someone point me in the right direction, there are most likely more things going wrong here but at the moment I am just stuck in where to even begin.
IdentityServer4 is just a backend implementation of OIDC; so, all you need to do is implement the flow in the client using the given APIs. I don't know what oidc-client.js file is but it is most likely doing the same thing that you could have implemented yourself. The flow itself is very simple:
React app prepares the request and redirects the user to the Auth server with client_id and redirect_uri (and state, nonce)
IdentityServer checks if the client_id and redirect_uri match.
If the user is not logged in, show a login box
If a consent form is necessary (similar to when you login via Facebook/Google in some apps), show the necessary interactions
If user is authenticated and authorized, redirect the page to the redirect_uri with new parameters. In your case, you the URL will look like this: https://example.com/cb#access_token=...&id_token=...&stuff-like-nonce-and-state
Now, the React app needs to parse the URL, access the values, and store the token somewhere to be used in future requests:
Easiest way to achieve the logic is to first set a route in the router that resolves into a component that will do the logic. This component can be "invisible." It doesn't even need to render anything. You can set the route like this:
<Route path="/cb" component={AuthorizeCallback} />
Then, implement OIDC client logic in AuthorizeCallback component. In the component, you just need to parse the URL. You can use location.hash to access #access_token=...&id_token=...&stuff-like-nonce-and-state part of the URL. You can use URLSearchParams or a 3rd party library like qs. Then, just store the value in somewhere (sessionStorage, localStorage, and if possible, cookies). Anything else you do is just implementation details. For example, in one of my apps, in order to remember the active page that user was on in the app, I store the value in sessionStorage and then use the value from that storage in AuthorizeCallback to redirect the user to the proper page. So, Auth server redirects to "/cb" that resolves to AuthorizeCallback and this component redirects to the desired location (or "/" if no location was set) based on where the user is.
Also, remember that if the Authorization server's session cookie is not expired, you will not need to relogin if the token is expired or deleted. This is useful if the token is expired but it can be problematic when you log out. That's why when you log out, you need to send a request to Authorization server to delete / expire the token immediately before deleting the token from your storage.

Resources