I am building a React Native app using Expo and AWS Cognito with AWS Amplify, and I am trying to enable signing in with Facebook, Google, etc. using AWS
I can create a user and sign in using Cognito APIs without any issue.
Using third-parties, though, requires using the Expo AuthSession functionality.
The functionality itself works fine, and I am able to get all the way to retrieving the proper tokens from my /oauth2/token endpoint.
However, as far as Amplify is concerned (and I am aware), the user is not signed in, so when I try to get Auth.currentAuthenticatedUser(), null is returned.
// Open URL in a browser
openURL = async (url) => {
let result = await AuthSession.startAsync({ authUrl: url })
this.getTokenbyCode(result.params.code)
};
getTokenbyCode = async (code) => {
const details = {
grant_type: 'authorization_code',
code,
client_id: '10eavoe3ufj2d70m5m3m2hl4pl',
redirect_uri: AuthSession.getRedirectUrl()
}
const formBody = Object.keys(details)
.map(
key => `${encodeURIComponent(key)}=${encodeURIComponent(details[key])}`
)
.join("&");
await fetch(
'https://presentor.auth.us-west-2.amazoncognito.com/oauth2/token',
{
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: formBody
}
)
.then(async (res) => {
console.log('res: ', res);
let resJSON = await res.json();
let idToken = await resJSON.id_token;
let decodedToken = jwt(idToken);
let userData = {
Username : decodedToken["cognito:username"],
Pool : Auth.userPool
}
})
.catch(error => {
console.log('error: ', error);
});
}
When I decode the token, I see the payload as I expect, but if I want to, for example, utilize the APIs to refresh the token if it expires, I have to workaround manually (check for expiration and retrieve a new token if it's expired).
Am I missing something basic?
Ok, I figured it out. Not sure if this is the right path, but it's pretty clean and it works, so I'm good with it.
Create CognitoIdToken, CognitoAccessToken, and CognitoRefreshToken objects using amazon-cognito-identity-js
Create a user session from those tokens
Create a user from that user session
await fetch(
'TOKEN ENDPOINT',
{
method: "POST",
headers: {
'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: formBody
}
)
.then(async (res) => {
const IdToken = new CognitoIdToken({ IdToken: tokenRequestJson.id_token });
const AccessToken = new CognitoAccessToken({ AccessToken: tokenRequestJson.access_token });
const RefreshToken = new CognitoRefreshToken({ RefreshToken: tokenRequestJson.refresh_token })
try {
let userSession = new CognitoUserSession({ IdToken, AccessToken, RefreshToken });
console.log('userSession: ', userSession);
const userData = {
Username: userSession.idToken.payload.email,
Pool: userPool
};
console.log('userData: ', userData);
cognitoUser = new CognitoUser(userData);
cognitoUser.setSignInUserSession(userSession);
cognitoUser.getSession((err, session) => { // You must run this to verify that session (internally)
if (session.isValid()) {
console.log('session is valid');
this.setState({user: cognitoUser})
this.props.navigation.navigate('AuthLoading')
} else {
console.log('session is not valid: ', session);
}
})
}
catch (FBSignInError) {
console.log('FBSignInError: ', FBSignInError)
}
})
.catch(fetchError => console.log('fetchError: ', fetchError))
Related
When the user logins, the access token is created and sent to the user, it is then stored in sessionStorage. Everything before this works fine. My problem is that I do not know how to use the access token to gain access to protected routes.
express app.js (smoothies is the protected route)
app.get('/smoothies', requireAuth, (req, res) => res.render('smoothies'));
authMiddleware.js
const User = require('../models/User');
const requireAuth = (req, res, next) => {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
// check json web token exists & is verified
if (token) {
jwt.verify(token, 'night of fire', (err, decodedToken) => {
if (err) {
console.log(err.message);
res.redirect('/login?err=auth');
} else {
console.log(decodedToken);
next();
}
});
} else {
res.redirect('/login?err=auth');
}
};
// check current user
module.exports = { requireAuth };
smoothies.ejs
var myHeaders = new Headers();
myHeaders.append("Authorization", `Bearer ${token}`);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("http://localhost:3000/smoothies", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
//Should I be even doing this fetch GET request on smoothie.ejs?
})
Smoothies is the protected route. When I try to use Postman and send a GET request to (/smoothies) using authorization : bearer token, it works and I am able to access /smoothies. However, if I try on the real application, I was denied access even with access token in my sessionStorage. When I console.log req.headers['authorization'], it was undefined so I am guessing my GET request from smoothie.ejs does not work. Does anyone know what is the solution?
I have an embarassing issue with cognito.
My authentication strategy works with current usage but when I try to run tests that sign up a new user and then log it in for an access to other APIs in my website
const authenticationData = {
Username: req.body.email,
Password: req.body.password,
};
const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
const poolData = {
UserPoolId: config.development.UserPoolId,
ClientId: config.development.ClientId,
TokenScopesArray : config.development.TokenScopesArray
};
const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
const userData = {
Username: req.body.email,
Pool: userPool,
TokenScopesArray : config.development.TokenScopesArray
};
const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
console.log('success')
token = result.getAccessToken().jwtToken;
const idToken = result.idToken.jwtToken;
console.log(token)
res.cookie("accessToken",token)
res.status(200).send(token);
},
onFailure: function (err) {
console.log(err)
res.status(404).send(err)
},`
Then when I try to authenticate with the following code :
app.use(function (req, res, next) {
var token = req.body.token || req.query.token || req.cookies.accessToken || req.headers['x-access-token'];
try {
if (token) {
let promise = new Promise((resolve, reject) => {
const data = null;
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.addEventListener("readystatechange", function () {
if (this.readyState === 4) {
console.log('response', this.responseText);
}
})
xhr.open("GET", "https://gridmanager.auth.us-east-1.amazoncognito.com/oauth2/userInfo");
xhr.setRequestHeader("Authorization", "Bearer " + token);
xhr.setRequestHeader("cache-control", "no-cache");
xhr.setRequestHeader("TokenScopesArray", config.development.TokenScopesArray)
xhr.send(data);
resolve(xhr.responseText)
})
.then(function (response) {
if (response != null) {
res.decoded = response
next();
}
else {
return res.status(404).send('User not authenticated')
}
})
}
else {
console.log('No token')
return res.status(403).send('No token')
}
} catch (error) {
// if there is no token
// return an error
console.log('error')
return res.status(403).send({
success: false,
message: error.message
});
}
I get the following error in xhr.responseText :
{"error":"invalid_token","error_description":"Access token does not contain openid scope"}
And when I log the accessToken I get in the login function, it only has 'aws.cognito.signin.user.admin'
I already tried to change the settings in my appclient but nothing works
Thanks for your help
Unfortunately, only access tokens issued by the Cognito hosted UI can include scopes other than aws.cognito.signin.user.admin. Cognito hosted UI supports OpenId Connect and Cognito API doesn't. It's a big gap in terms of functionality provided by those two. The /oauth2/userInfo endpoint is part of the Hosted UI and it also follows the OpenID Connect spec.
Why do you want to call the /oauth2/userInfo endpoint when you have access to the id_token? The id_token payload has all the information about the user that /oauth2/userInfo would return.
I'm trying to implement a simple OpenID Connect react-admin login using gitlab as OAuth2 service provider.
most of the react-admin examples about OpenID is simple username/password login. But OpenID Connect will do several redirects, and what I come with is make python/flask server redirect to http://example.com/#/login?token=<token>, and make react-admin to parsed the URL, and set token in localStorage.
basically is somethings like below:
(({ theme, location, userLogin } ) => {
let params = queryString.parse(location.search);
if (params.token) {
userLogin({token: params.token});
}
return (
<MuiThemeProvider theme={ theme }>
<Button href={ '/api/gitlab/login' }>
Login via GitLab
</Button>
</MuiThemeProvider>
);
});
Obviously, that is not good enough, I want to have some advice about how can I improve this?
I assume you followed this example https://marmelab.com/react-admin/Authentication.html, that does not cover the OAuth2 password grant.
// in src/authProvider.js
import { AUTH_LOGIN } from 'react-admin';
export default (type, params) => {
if (type === AUTH_LOGIN) {
const { username, password } = params;
const request = new Request('https://mydomain.example.com/authenticate', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
})
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
})
.then(({ token }) => {
localStorage.setItem('token', token);
});
}
return Promise.resolve();
}
The GitLab guys / girls describe what grants they provide.
https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow
Here is the example how you can get an access token using curl:
echo 'grant_type=password&username=<your_username>&password=<your_password>' > auth.txt
curl --data "#auth.txt" --request POST https://gitlab.com/oauth/token
With the access token you can get some information from the user that is also described here: https://docs.gitlab.com/ee/api/oauth2.html#access-gitlab-api-with-access-token
Here is the example how you can get information from GitLab with the access token that you got from the previous call using curl:
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.com/api/v4/user
With some small adjustments in the react-admin example you can use the password credentials flow.
Here you can find the example that works with GitLab:
https://gist.github.com/rilleralle/b28574ec1c4cfe10ec7b05809514344b
import { AUTH_LOGIN } from 'react-admin';
export default (type, params) => {
if (type === AUTH_LOGIN) {
const { username, password } = params;
const oAuthParams = {
grant_type: "password",
username,
password
}
const body = Object.keys(oAuthParams).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(oAuthParams[key]);
}).join('&');
const request = new Request('https://gitlab.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body
})
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
})
.then(( {access_token} ) => {
localStorage.setItem('token', access_token);
});
}
return Promise.resolve();
}
I hope this helps you.
Cheers
Ralf
Here an good example using oauth flow in react-admin auth provider:
https://github.com/marmelab/ra-example-oauth/blob/master/app/src/authProvider.js
working on connecting users to google, and we're trying to get their access and refresh tokens from the google api, and we're getting an issue exchanging the OAuth2 Code for tokens. Both sets of code have the same error.
I initialize the gapi client and fill in the information needed like so:
gapi.load('client:auth2', _ => {
gapi.client.init({
'apiKey': 'omitted for security',
clientId: 'omitted for security',
'scope': 'https://www.googleapis.com/auth/drive',
'discoveryDocs': ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest']
}).then(_ => {
gapi.auth2.getAuthInstance().grantOfflineAccess().then(resp => {
if(resp.code){
gapi.client.request({
path: 'https://www.googleapis.com/oauth2/v4/token',
method: 'post',
params: {code: resp.code},
body: {
code: resp.code,
client_id: opts.clientId,
client_secret: 'omitted for security',
grant_type: 'authorization_code',
redirect_uri: 'omitted for security',
access_type: 'offline'
},
}).then((onfulfill, onreject, context) => {
console.log('fulfilled', onfulfill);
console.log('rejected: ', onreject);
console.log('context', context);
}).catch(err => console.error(err.body));
}
});
});
});
What I'm trying to do in the .then() is to call the token endpoint to exchange the code in the response for a refresh and access token to store in my back end and the user's local storage.
I get this error response from both versions of the code. (better, more reliable code is provided here.)
{ "error": "redirect_uri_mismatch", "error_description": "Bad
Request" }
I also have a backend setup stashed as a last resort that accepts the code from gapi.auth2.getAuthInstance().grantOfflineAccess() calls the token endpoint, and returns the access_token and refresh_token to the client.
This code is similar, but not quite. instead of using the google api library, I used fetch, and it works fine. (Fetch and XHR on the front end have the same issues as the gapi.client.request function....)
const gConfig = require('./basic.json');
const scopes = ['https://www.googleapis.com/auth/drive'];
const { client_id, client_secret, redirect_uris } = gConfig.web;
const authClient = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
app.post('/', (req, res) => {
const { code } = req.body;
console.log('Received Code From Request: ', code);
let data = { code , client_id, client_secret,redirect_uri: redirect_uris[0], grant_type: 'refresh_token'};
let encodedParams = Object.keys(data).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k])).join('&');
fetch(
`https://www.googleapis.com/oauth2/v4/token?code=${code}`,
{ method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: encodedParams }
).then((res) => {
console.log('called the api with fetch');
console.dir(res.json());
});
authClient.getToken(code, (err, token) => {
if (err) {
console.error(err);
res.status(500).json(err);
}
// console.dir(token);
console.log('TOKEN: =>', token);
res.json(token);
});
});
Is there anyone that's done this on the front end successfully?
You can't get a refresh token in a browser. Your example code would only work on a server. To do oauth at the client you should request "token" instead of "code".
For my react-native app I need to make sure that before every fetch request to server the use-case below should be executed
-> check the expire date of token that is saved to redux.
--> If token is not expired, app keeps going on with requested fetch to server
--> If token expired, app immediately makes new request to refresh token without making user knows it. After successfully refreshing token, app keeps going on with requested fetch to server
I tried to implement middleware with redux-thunk, but I do not know whether it's good design or not. I just need someone experienced with redux and react to give me feedback over my middleware code.
This is how I make requests to server oveer my app's component through dispatching the checkTokenAndFetch - action creater.
url = "https://———————";
requestOptions = {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + this.props.token
}
};
dispatch(authActions.checkTokenAndFetch(url, requestOptions))
.then((data) => {
})
here is action creator - checkTokenAndFetch located in authActions.js
file where my actions located
function checkTokenAndFetch(url, requestOptions){
return dispatch => {
if(authServices.isTokenExpired()){
console.log("TOKEN EXPIRED");
authServices.refreshToken()
.then(
refreshToken => {
var arr = refreshToken.split('.');
decodedToken = base64.decode(arr[1]);
newTokenExpDate = JSON.parse(decodedToken).exp;
dispatch(writeTokenToRedux(refreshToken,newTokenExpDate));
},
error => {
Alert.alert("TOKEN refresh failed","Login Again");
Actions.login();
}
);
}
else{
console.log("TOKEN IS FRESH");
}
return authServices.fetchForUFS(url, requestOptions)
.then(
response => {
return response;
},
error => {
}
)
;
}
}
Here is isTokenExpired and refreshToken functions that I call for case of token expire, located in another file named authServices.js.
function isTokenExpired(){
var newState = store.getState();
var milliseconds = (new Date).getTime();
var exDate = newState.tokenExpDate;
return milliseconds>exDate*1000
}
function refreshToken(){
var refreshToken = store.getState();
return fetch('https://—————————', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + refreshToken.token
}
})
.then((response) => {
return response._bodyText;
})
.catch((error) => {
return error;
})
}
and my fetchForUFS function in authServices.js to make a call to server after completeing token-check(refresh) stuff.
function fetchForUFS(url,requestOptions){
return fetch(url, requestOptions)
.then((response) => {
return response.json();
})
.then((responseData) =>{
return responseData;
})
.catch((error) => {
})
}
I've read tons of redux-thunk, redux-promise and middleware documentation and I'm yet not sure whether I am implementing middleware logic truly?