Everything was working and for no apparent reason now I am getting a 400 Bad Request when I attempt to sign in.
Very basic application
// App.js
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Security, SecureRoute, LoginCallback } from '@okta/okta-react';
import { OktaAuth } from '@okta/okta-auth-js';
import { Home } from './Home';
const CALLBACK_PATH = '/implicit/callback';
const config = {
clientId: 'client_id',
issuer: 'https://Okta_domain/oauth2/default',
redirectUri: 'http://localhost:3000/implicit/callback',
scopes: ['openid', 'profile', 'email'],
pkce: true,
};
const oktaAuth = new OktaAuth(config);
const App = () => {
return (
<Router>
<Security oktaAuth={oktaAuth}>
<Switch>
<Route path="/" exact component={Home} />
<SecureRoute path="/private" />
<Route path={CALLBACK_PATH} component={LoginCallback} />
</Switch>
</Security>
</Router>
);
};
export default App;
// Home.js
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useOktaAuth } from '@okta/okta-react';
export const Home = () => {
const [userInfo, setUserInfo] = useState(null);
const { authState, oktaAuth } = useOktaAuth();
useEffect(() => {
if (!authState.isAuthenticated) {
// When user isn't authenticated, forget any user info
setUserInfo(null);
} else {
oktaAuth.getUser().then((info) => setUserInfo(info));
}
}, [authState, oktaAuth]);
const login = async () => {
oktaAuth.signInWithRedirect();
};
if (authState.isPending) {
return <div>Loading...</div>;
}
return (
<div>
{authState.isAuthenticated && !userInfo && (
<div>Loading user information...</div>
)}
{authState.isAuthenticated && userInfo && (
<div>
<p>Welcome back {userInfo.name}</p>
</div>
)}
{!authState.isAuthenticated && (
<div>
<Link onClick={login}>Login</Link>
</div>
)}
</div>
);
};
Application configuration:
Clicking logon gives the bad request for url:
https://dev-812909.okta.com/oauth2/default/v1/authorize?client_id=xxxx&code_challenge=seXLoVJmSSopX6Q1A_2KuSw8SkDroSWFvwCygNWZ4bg&code_challenge_method=S256&nonce=5I2H9IINx2WwZIL9BMIPx0cYtqZPePh140nvwPBxub3tpegelypkxehGXJJpceeP&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fimplicit%2Fcallback&response_type=code&state=vwAqRTGkAuIpQtvek35KFzgO2FY3MGjHknoykGPuJnGnEcDfvYzEeOXnMPB2QtIC&scope=openid%20profile%20email
What has changed that this would suddenly not work?
PS. I also have a backend API server that still works perfectly.
Regards
mraible
December 21, 2020, 9:32pm
2
When I’ve received a generic 400 error without any additional information, it’s usually because my client ID is invalid.
Hi @mraible
Astute observation.
Interesting enough, you are on the money. But why would that have suddenly changed. It has literally been working for a year.
Even more interesting, updating that does not resolve the error.
I now get
.
In the network log I am getting a 401 for POST https://dev-812909.okta.com/oauth2/default/v1/token
{error: "invalid_client",…}
error: "invalid_client"
error_description: "Client authentication failed. Either the client or the client credentials are invalid."
I have copied and pasted the values
Thanks
mraible
December 21, 2020, 10:25pm
4
I don’t know why it would change. Does someone else have access to your org, or is it just you? If someone else has access, maybe they deleted the app with that client ID?
This is my dev org so no.
mraible
December 21, 2020, 10:27pm
6
One thing I just noticed in your app settings screenshot is that your app is of type “Web”, yet you show code for React. For React, and other frontend frameworks, your app should be a “SPA” app.
It was originally created as an SPA. However, when I modified it to allow the backend (node.js with express) it changed to web.
I am thinking I just have to delete it and recreate it and see how that goes
I need the client ID and secret. That can only be used in the type of web
mraible
December 21, 2020, 10:44pm
9
If you’re putting a client secret in your JavaScript frontend app, you’re doing it wrong. In fact, none of our frontend SDKs even support a client secret in their configuration. Backend apps (like Express.js, .NET Core, and Spring Boot in Javaland) can have a client secret because they can send it on a back channel and use it as a form of authentication to Okta.
If you want to have an app that uses React on the frontend, and Express on the backend, you can package them together and have the authentication flow happen on the backend. I have a post that shows how to do this for Angular and Spring Boot .
If you want to have a React app that can authenticate with Okta (and is separate from your backend), you have to use a SPA app. For your backend, you can often just use the issuer (you don’t even need a client ID) to validate the JWT that’s sent as an access token. However, if you want to do an OAuth flow on your backend, then you will need a client ID + secret and an app of type “Web”.
I’m sorry it’s so confusing.
Sorry - bit confusing there. I am using the client id and secret in postman to test the backend apis
The backend uses the client id and domain in a middleware to validate that the token sent by the front end is valid, extract the user information and determine the application roles (defined within the application).
'use strict';
const OktaJwtVerifier = require('@okta/jwt-verifier');
const log = require('./logging');
const userDAL = require('../components/user/userDAL');
const oktaJwtVerifier = new OktaJwtVerifier({
issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
clientId: process.env.OKTA_CLIENT_ID
});
const checkToken = (req, res, next) => {
const bearerHeader = req.headers['authorization'];
const diagnosis = {
bearerHeader,
functionName: 'checkToken',
headers: JSON.stringify(req.headers)
};
if (bearerHeader) {
const bearer = bearerHeader.split(' ');
const bearerToken = bearer[1];
// verify the token
oktaJwtVerifier
.verifyAccessToken(bearerToken, 'api://default')
.then(async jwt => {
// token is valid - verify the member is a valid user of the system
const oktaSequence = jwt.claims.uid ? jwt.claims.uid : jwt.claims.sub;
if (oktaSequence) {
const user = await userDAL.userByOktaSequence({oktaSequence});
if (user.length) {
req.user = user[0];
return next();
} else {
const errMgs = 'User not registered in the application';
log.error(
{ ...diagnosis },
errMgs
);
return res.status(401).json({ error: errMgs });
}
} else {
const errMgs = `Invalid Okta Sequence ${oktaSequence}`;
log.error(
{ ...diagnosis },
errMgs
);
return res.status(401).json({ error: errMgs });
}
})
.catch(err => {
const errMgs = 'Invalid token';
log.error(
{ ...diagnosis },
errMgs
);
return res.status(401).json({
err,
error: errMgs
});
});
} else {
const errMsg = 'Missing authorization';
log.error(
{ ...diagnosis },
errMsg
);
return res.status(401).json({ error: errMsg });
}
};
module.exports = {checkToken};
In postman I use the client id and secret as part of the pre-request script
if (pm.environment.name == 'local') {
return;
}
const authStr = pm.environment.get('clientId') + ':' + pm.environment.get('clientSecret');
const encodedAuthStr = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authStr));
const echoPostRequest = {
url: `${pm.environment.get('url')}/oauth2/${pm.environment.get('authorizationServerId')}/v1/token`,
method: 'POST',
header: {
"Accept":"application/json",
"Authorization":`Basic ${encodedAuthStr}`,
"Content-type":"application/x-www-form-urlencoded"
},
body: {
mode: 'urlencoded',
urlencoded: [
{key: "grant_type", value: "client_credentials", disabled: false},
{key: "scope", value: `${pm.environment.get('scopes')}`, disabled: false}
]
}
};
console.log(echoPostRequest);
let getToken = true;
if (!pm.environment.get('accessTokenExpiry') ||
!pm.environment.get('accessToken')) {
console.log('Token or expiry date are missing');
} else if (pm.environment.get('accessTokenExpiry') <= (new Date()).getTime()) {
console.log('Token is expired');
} else {
getToken = false;
console.log('Token and expiry date are all good');
}
if (getToken === true) {
pm.sendRequest(echoPostRequest, function(err, res) {
if (err) {
console.error(err);
throw new Error("An error has occurred. Check logs");
}
if(err === null) {
console.log(res.json());
switch (res.code) {
case 200: {
console.log('Saving the token and expiry date');
const responseJson = res.json();
pm.environment.set('accessToken', responseJson.access_token);
let expiryDate = new Date();
expiryDate.setSeconds(expiryDate.getSeconds() + responseJson.expires_in);
pm.environment.set('accessTokenExpiry', expiryDate.getTime());
return;
}
case 400: {
throw new Error("Forbidden");
}
case 401: {
throw new Error("Invalid refresh token");
}
case 403: {
throw new Error("Blacklisted token");
}
default: {
throw new Error("Unexpected error code: " + res.code);
}
}
}
});
}
I should also note that my prod environment does not return as detailed error messages removing the disclosure vulnerability
mraible
December 21, 2020, 11:30pm
12
OK, this is good information. This doesn’t change the fact that if you want your React client app to be able to get an access token, you’ll still need to have it as a SPA app. As long as both apps use the same Okta org, they’ll be able to communicate with each other.
OK. So the solution is to create a new SPA application?
Again, I am confused as to why this has been working for a year
Hi @mraible ,
Just to complicate matters.
The instructions at https://developer.okta.com/docs/guides/sign-into-spa/angular/create-okta-application/ say to create the SPA application, and in step 4 select grant types Authorization Code and Refresh Tokens
Yet there is no option to do so
mraible
December 22, 2020, 1:39am
15
Refresh tokens for SPA apps is an early access feature. If you click the Refresh token rotation link, there’s an EA (Early Access) label at the top. We just released it earlier this month. If you send an email to developers@okta.com , you can get it turned on. It does seem like an oversight to have it in the doc without an explanation.
Thanks, I will follow that line.
In the meantime I have recreated the application and it appears to be functioning.
1 Like
system
Closed
December 23, 2020, 2:06am
17
This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.