I am trying to use OAuth2 to authenticate with Twitter on my React-Native/Expo app, using the expo-auth-session package (Expo's guide for Twitter OAuth).
The Twitter OAuth2 flow works in two steps : first the user authorizes the app to access their account, which returns a code, then we exchange that code for an access token.
I am stuck at the second step.
Whenever I try to exchange the code for a token, using expo's exchangeCodeAsync function with these parameters :
exchangeCodeAsync({
clientId: '<CLIENT_ID>',
redirectUri: makeRedirectUri({
scheme: 'my.app',
useProxy
}),
code: response.params.code,
extraParams: {
client_id: "<CLIENT_ID>",
code_verifier: request?.codeVerifier || '',
redirect_uri: makeRedirectUri({
scheme: 'my.app',
useProxy
}),
grant_type: "authorization_code",
},
}, discovery)
I get this error :
Possible Unhandled Promise Rejection (id: 0):
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
What I understand is that the function makes a request to the /oauth2/token route of the Twitter API under the hood, but because of some wrong parameter it returns an HTML error code :
Something went wrong, but don’t fret — let’s give it another shot.
Since the error message is so vague, I have no idea of what is wrong with my request.
I assume that since I have completed the authorization step, the redirect_uri is properly configured. I have also made sure that the format for the "code_verifier" and "code" fields were valid, according to Twitter's documentation.
Here is an Expo Snack to show the complete App.js setup I am using (I've also configured app.json with a custom scheme )
https://snack.expo.dev/lWYp82Pwq
I was able to get a working Twitter login using a bit of a cobbled-together code path:
const twitterDiscovery = {
authorizationEndpoint: "https://twitter.com/i/oauth2/authorize",
tokenEndpoint: "https://api.twitter.com/2/oauth2/token",
}
...
const redirectUri = AuthSession.makeRedirectUri({
scheme: APP_URI_SCHEME,
useProxy: true,
})
const reqConfig = {
clientId: TWITTER_CLIENT_ID,
redirectUri,
usePKCE: true,
scopes: twitterScopes,
}
const authReq = new AuthRequest(reqConfig)
const authUrl = await authReq.makeAuthUrlAsync(twitterDiscovery)
const loginResult = await AuthSession.startAsync({ authUrl, returnUrl })
if (loginResult.type !== "success") {
throw new Error("...")
}
const tokenRequest = new AccessTokenRequest({
...reqConfig,
code: loginResult.params.code,
extraParams: {
code_verifier: authReq.codeVerifier,
},
})
const tokenResult = await tokenRequest.performAsync(twitterDiscovery)
This could obviously be cleaned up a bit, but it does appear to be functional, so I wanted to post even though it's not pretty.
Related
I would like to implement Csrf protection with NestJS and Quasar.
But I think I misunderstand something...
btw I'm not doing SSR, so I don't send the form from the back to the view.
Here is the NestJs back-end code:
async function bootstrap() {
const PORT = process.env.PORT;
const app = await NestFactory.create(AppModule, {
cors: true,
bodyParser: false,
});
console.log(`your App is listening on port ${PORT}`);
// Added Cookie-parser to user csurf packages
// Prevent CSRF attack
app.use(cookieParser());
app.use(csurf({ cookie: true }));
await app.listen(PORT);
}
bootstrap();
So I'm just using CookieParser and csurf package.
On my login page I call a "csrf endpoint" just to send a cookie to the view, to send it back with the post call (login).
I still get the "invalid csrf token" AND a CORS error and don't know why....(see screen below), any suggestions to make it works ?
When I try to login, error in the browser:
And error in the back-end:
Same error if I try a request with insomnia.
I thought that the CSRF token is attached to the "web browser" to go back to the back-end with nest request, so why I'm still getting this error ?
Insomnia send the cookie automatically with the right request so the token should go back to the back-end.
Any idea ?
Regards
EDIT:
After many times reading docs, It seems that CSRF protection is for SSR only ? No need to add csrf security with SPA ? Could anyone can confirm ?
EDIT: Here's another work:
The purpose here is to send a request before login to get a csrf token that I can put into a cookie to resend when I login with a POST method.
Here is my endpoint:
import { Controller, Get, Req, Res, HttpCode, Query } from "#nestjs/common";
#Controller("csrf")
export class SecurityController {
#Get("")
#HttpCode(200)
async getNewToken(#Req() req, #Res() res) {
const csrfToken = req.csrfToken();
res.send({ csrfToken });
}
}
Here is what I've done into my main.ts file (I'll explain below):
async function bootstrap() {
const PORT = process.env.PORT;
const app = await NestFactory.create(AppModule, {
cors: {
origin: "*",
methods: ["GET,HEAD,OPTIONS,POST,PUT"],
allowedHeaders: [
"Content-Type",
"X-CSRF-TOKEN",
"access-control-allow-methods",
"Access-Control-Allow-Origin",
"access-control-allow-credentials",
"access-control-allow-headers",
],
credentials: true,
},
bodyParser: false,
});
app.use(cookieParser());
app.use(csurf({ cookie: true }));
console.log(`your App is listening on port ${PORT}`);
await app.listen(PORT);
}
bootstrap();
And here my axiosInstance Interceptors of the request in my VueJS frontend:
axiosInstance.interceptors.request.use(
(req) => {
const token = Cookies.get('my_cookie')
if (token) {
req.headers.common['Authorization'] = 'Bearer ' + token.access_token
}
req.headers['Access-Control-Allow-Origin'] = '*'
req.headers['Access-Control-Allow-Credentials'] = 'true'
req.headers['Access-Control-Allow-Methods'] = 'GET,HEAD,OPTIONS,POST,PUT'
req.headers['Access-Control-Allow-Headers'] =
'access-control-allow-credentials,access-control-allow-headers,access-control-allow-methods,access-control-allow-origin,content-type,x-csrf-token'
const csrfToken = Cookies.get('X-CSRF-TOKEN')
if (csrfToken) {
req.headers['X-CSRF-TOKEN'] = csrfToken
console.log(req)
}
return req
},
(err) => {
console.log(err)
},
Here the same for repsonse:
axiosInstance.interceptors.response.use(
(response) => {
if (response?.data?.csrfToken) {
const {
data: { csrfToken },
} = response
Cookies.set('X-CSRF-TOKEN', csrfToken)
}
return response
},
And inside my login I make a call on the mounted function of my login component:
async mounted() {
const result = await securityService.getCsrf()
},
So now to explain:
As I said I'm not building a SSR project, that's why I want to send the token into a classic axios reponse and store it in a Cookie (this part is for test I heard that storing a csrf token into a classic cookie is not the right way.)
And for each next request I get the csrf token and "attach" it to the request into the headers, making my headers "custom".
Here is a problem I don't know how to make custom headers works with nestJS and CORS, that's why I try many thing with CORS options in NestJS and writte some custome header before the request go to the back-end but without success, I've got the same error message:
I'm a bit confuse about this problem and CORS/CSRF is a big deal for spa, my questions still the same, with CORS and SameSite cookie attributes, and my api is in a subdomain of my front-end, is it really necessary to make a anti-csrf pattern ?
Btw how can I make my custom headers working and why CORS say to me there is no "Access-Control-Allow-Origin" header but there is:
try to generate csrf token and pass to front on each petition
// main.ts - from NestJs - Backend
// after app.use(csurf({ cookie: true }))
app.use((req: any, res: any, next: any) => {
const token = req.csrfToken()
res.cookie("XSRF-TOKEN", token)
res.locals.csrfToken = token
next()
})
from: https://github.com/nestjs/nest/issues/6552#issuecomment-1175270849
I'm doing a project with vue, nuxt and keycloak as server for token, axios as http client and #nuxtjs/auth-next module for keycloak access.
I'm using a public client so I don't have a secret key which is the most recommended.
The part of getting the token and talking to the backend is working.
But as it is a public client it has no refresh token.
Searching the internet, a recommendation would be to post from time to time to the keycloak /token endpoint, passing the current token, to fetch a new token.
To perform this post, it doesn't work to pass json, having to pass application/x-www-form-urlencoded.
But it generates an error saying that the parameter was not passed.
On the internet they recommended passing it as url string, but then it generates an error on the keycloak server, as a parameter that is too long, because of the current token that is passed.
Below is the code used to try to fetch a new token.
This code is being called on a test-only button.
If anyone can help, I appreciate it.
const token = this.$auth.strategy.token.get()
const header = {
"Content-Type": "application/x-www-form-urlencoded"
}
const body = {
grant_type: "authorization_code",
client_id: "projeto-ui",
code: token
}
this.$axios ( {
url: process.env.tokenUrl,
method: 'post',
data: body,
headers: header
} )
.then( (res) => {
console.log(res);
})
.catch((error) => {
console.log(error);
} );
Good afternoon people.
Below is the solution to the problem:
On the keycloak server:
it was necessary to put false the part of the implicit flow.
it was necessary to add web-origins: http://localhost:3000, to allow CORS origins.
In nuxt.config.js it was necessary to modify the configuration, as below:
auth: {
strategies: {
keycloak: {
scheme: 'oauth2',
...
responseType: 'code',
grantType: 'authorization_code',
codeChallengeMethod: 'S256'
}
}
}
While I try to make an custome backend API call with the accessToken I receive from login success using react-native-app-auth, the call fails and returns a 401 error.
My login succeeds with no error but the response I get after login, consists of empty scopes scopes: []
Below is my config,
const AuthorizationConfig = {
appId: 'XXXXX',
tenantId: 'XXXX',
appScopes: [
'openid',
'offline_access',
'profile',
'User.Read',
'api://XXXX/access_as_user',
],
};
export const config: any = {
warmAndPrefetchChrome: true,
clientId: AuthorizationConfig.appId,
redirectUrl: 'com.dcomobile://react-native-auth/',
scopes: AuthorizationConfig.appScopes,
additionalParameters: {prompt: 'select_account'},
serviceConfiguration: {
authorizationEndpoint:
'https://login.microsoftonline.com/' +
AuthorizationConfig.tenantId +
'/oauth2/v2.0/authorize',
tokenEndpoint:
'https://login.microsoftonline.com/' +
AuthorizationConfig.tenantId +
'/oauth2/v2.0/token',
},
};
I call this function to authorize which in turn opens a Microsoft Login window in a mobile browser
const result = await authorize(config);
result.accessToken contains the token which I have to append to the header section in my API call. The token is a Bearer token
I'm wondering if my scopes are wrong as it as scopes for both MSGraph and custom API.
Any leads would be helpful! TIA
I'm using vue cli and I try to get album list from my google photos account.
I'm using axios-oauth-client and I try to implement this code:
const axios = require('axios');
const oauth = require('axios-oauth-client');
const getAuthorizationCode = oauth.client(axios.create(), {
url: 'https://oauth.com/2.0/token',
grant_type: 'authorization_code',
client_id: 'foo',
client_secret: 'bar',
redirect_uri: '...',
code: '...',
scope: 'baz',
});
const auth = await getAuthorizationCode(); // => { "access_token": "...", "expires_in": 900, ... }
As described in here and I can't understand how do I get the authorization code aka code in this implementation.
I managed to do this call in postman but I'm unable to do it using axios.
Postman
My Code
async function getToken() {
const getAuthorizationCode = oauth.client(axios.create(), {
url: "https://oauth2.googleapis.com/token",
grant_type: "authorization_code",
client_id:
"**********.apps.googleusercontent.com",
client_secret: "***********",
redirect_uri: "http://localhost:8080/oauth2/callback",
code: "...",
scope: "https://www.googleapis.com/auth/photoslibrary.readonly"
});
const auth = await getAuthorizationCode(); // => { "access_token": "...", "expires_in": 900, ... }
console.log(auth);
}
getToken();
What am I doing wrong?
Thanks!
UPDATE
I still didn't manage to make it works but I found this answer which I will try to check.
UPDATE#2
Eventually I ended up using google documentation for Oauth2 using php and I took their git example as a base project.
In order to do it I also needed to use database to save the tokens and especially the refresh token which I use to refresh the access token every time I receive 401.
I'm starting to work on an express API using graphql with apollo-server-express and graphql-tools. My register user process steps are:
User submit user name, email and password.
Server send an email to user by Mailgun with unique link generated by uuid.
User follow the link to verify the registration.
But I'm in struggle at how to bind the mutation in the resolver. See snippets:
server.js
const buildOptions = async (req, res, done) => {
const user = await authenticate(req, mongo.Users)
return {
schema,
context: {
dataloaders: buildDataloaders(mongo),
mongo,
user
},
}
done()
}
// JWT setting
app.use('/graphAPI',
jwt({
secret: JWT_SECRET,
credentialsRequired: false,
}),
graphqlExpress(buildOptions),
res => data => res.send(JSON.stringify(data))
)
Mutation on resolver
signupUser: async (root, data, {mongo: { Users }}) => {
// Check existed accounts,
// if account is not exist, assign new account
const existed = await Users.findOne({email: data.email})
if (!existed) {
// create a token for sending email
const registrationToken = {
token: uuid.v4(),
created_at: new Date(),
expireAfterSeconds: 3600000 * 6 // half day
}
const newUser = {
name: data.name,
email: data.email,
password: await bcrypt.hash(data.password, 10),
created_at: new Date(),
verification_token: registrationToken,
is_verified: false,
}
const response = await Users.insert(newUser)
// send and email to user
await verifyEmail(newUser)
return Object.assign({id: response.insertedIds[0]}, newUser)
}
// Throw error when account existed
const error = new Error('Email existed')
error.status = 409
throw error
},
// VERIFY USER
// Set verify to true (after user click on the link)
// Add user to mailist
verifiedUser: async (root, data, {mongo: { Users }}) => {
await Users.updateOne(
{ email: data.email },
{
set: {is_verified: true},
unset: {verification_token: {token: ''}}
}
)
},
route config
routes.get('/verify?:token', (req, res, next) => {
res.render('verified', {title: 'Success'})
})
the route config is where I stuck, because the object is passed to all resolvers via the context inside graphqlExpress
Any one help me out or suggest for me any articles related. Thanks so much.
You will need 3 graphql endpoints and 1 apollo http endpoint for proper workflow.
Optionally you can combine 3 graphql endpoints in one, but then it will be a one big function with a lot of different responsibilities.
1# graphql endpoint: changepass-request
expects email param
check if user with such email found in db:
generate code
save it in the local account node
send code to the user email with http link to confirm code:
http://yoursite.com/auth/verify?code=1234
return redirect_uri: http://yoursite.com/auth/confirm-code
for UI page with prompt for confirmation code
2# graphql endpoint: changepass-confirm
expects code param:
if user with such code found in db, return redirect_uri to UI page with prompt for new pass with confirmation code in params: http://yoursite.com/auth/change-pass?code=1234
3# graphql endpoint: changepass-complete
expects code and new pass:
hash new password
search in db for local account with such code
3a. if not found:
return error with redirect_uri to login page:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
3b. if found:
change password for new, return success status with redirect_uri to login page:
http://yoursite.com/auth?success=true&message="ok"
4# apollo HTTP endpoint: http://yoursite.com/auth/verify?code=1234
if no code provided:
redirect to UI registration page with error message in params:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
if code provided: search in db for local account with such code
1a. if user not found:
redirect to reg ui with err mess in params:
http://yoursite.com/auth?success=false&message="Confirmation code is not correct, try again."
1.b if user found:
redirect to ui page with new password prompt and attach new code to params
I didn't put any code above, so you can use this workflow in other auth scenarios.
It seems like rather than utilizing the verifiedUser endpoint, it would be simpler to just keep that logic inside the controller for the /verify route. Something like:
routes.get('/verify?:token', (req, res) => {
Users.updateOne(
{ verification_token: { token } },
{
$set: {is_verified: true},
$unset: {verification_token: {token: ''}}
},
(err, data) => {
const status = err ? 'Failure' : 'Success'
res.render('verified', {title: status})
}
)
})