How to store user session after signup in remix-auth? - authentication

I'm using https://github.com/sergiodxa/remix-auth-github.
I was wondering, if I have a signup screen, what is the correct method to store the user session once the signup is finished, so the user does not have to log in again?
To explain this more clearly. Imagine I have a function:
async function signup(userInfo) {
await DB.insertUser(userInfo)
}
Once this is finished, I would like to store the user's session information inside of the cookie instead of having them log in again. What's the best method to do this?

Author here, if your Authenticator stores the data returned by signup then you could do something like this:
export let action: ActionFunction = async ({ request }) => {
// get the user info from the formData, however you are doing it, this
// depends on your app
let userInfo = await getUserInfo(request)
// register the user with your function
let user = await signup(userInfo)
// get the session object from the cookie header, the getSession should
// be the same returned by the sessionStorage you pass to Authenticator
let session = await getSession(request.headers.get("cookie"))
// store the user in the session using the sessionKey of the
// Authenticator, this will ensure the Authenticator isAuthenticated
// method will be able to access it
session.set(authenticator.sessionKey, user)
// redirect the user somewhere else, the important part is the session
// commit, you could also return a json response with this header
return redirect("/somewhere", {
headers: { "Set-Cookie": await commitSession(session) },
});
}
This way, now when you call authenticator.isAuthenticated(request) it will work and return the user object.

Related

react native setState inside an async asyncStorage function

I am using the expo-auth-session package to make a request to the Spotify API to get access tokens, then saving to AsyncStorage.
A save function that stores the token in AsyncStorage:
const save = async (token) => {
try{
AsyncStorage.setItem('access_token', token)
}
catch(error){
console.log(error)
}
}
A getItem function that gets the access token value from AsyncStorage, and sets that value to the spotifyAccessToken state
const [spotifyAccessToken, setSpotifyAccessToken] = useState('');
const getItem = async () => {
try{
const token = await AsyncStorage.getItem('access_token')
setSpotifyAccessToken(token);
}
catch(error){
console.log(error)
}
}
Using the useAuthRequest from expo-auth-session to make a request to Spotify API, the request code below works.
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: "https://accounts.spotify.com/api/token"
};
const [request, response, promptAsync] = useAuthRequest({
// responseType: ResponseType.Token,
responseType: 'code',
clientId: client_id,
//clientSecret: client_secret,
scopes: ['user-read-recently-played'],
usePKCE: false,
redirectUri: REDIRECT_URI
}, discovery)
useEffect(() => {
if (response?.type === 'success'){
//console.log(response.params.code);
axios.request({
method: 'POST',
url: 'https://accounts.spotify.com/api/token',
headers: {
'content-type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${new Buffer.from(`${client_id}:${client_secret}`).toString('base64')}`,
},
data: {
grant_type: 'authorization_code',
code: response.params.code,
redirect_uri: REDIRECT_URI
}
}).then(res => {
save(res.data.access_token);
}).catch(err => {console.log(err)})
}
},
[response]);
A button that triggers the user to login using Spotify account, after authenticating, it redirects back to this component screen, however, I want the text below the button to be displayed from "Loading..." to the spotifyAccessToken immediately after it redirects to the component screen, but it wouldn't. After I re-run my application, the token is displayed, which means it was successfully stored in AsyncStorage, but didn't update the state immediately. How can solve this? Thanks.
const [spotifyAccessToken, setSpotifyAccessToken] = useState(null);
useEffect(()=>{
//clearTokens();
// console.log('storage: ' + getValueForfor('access_token'))
// console.log('state: ' + spotifyAccessToken)
getItem()
}, [spotifyAccessToken])
<Button title='login to spotify' onPress={() => promptAsync()}/>
{spotifyAccessToken != '' ? <Text> {spotifyAccessToken} </Text> : <Text> Loading... </Text>}
This might be happening if you are redirecting to the component with getItem too early: before the AsyncStorage is done saving the token. Due to this, at the initial render of the component(with getItem), AsyncStorage.getItem might be getting the old value of access_token and not the updated one.
To possibly fix this issue, try redirecting to the next component only after AsyncStorage.setItem promise is resolved completely. Something like this:
This is how your save function should look like: it should return a Promise value:
const save = async (token) => {
try{
await AsyncStorage.setItem('access_token', token)
}
catch(error){
console.log(error)
}
}
And redirect to the next component after the save return promise value is resolved:
...
).then(async (res) => {
await save(res.data.access_token);
// Redirect here, after save is resolved
})...
Answering the question you asked in the comments to this answer:
it's not working still, you said that the save function should return a promise value, where in the code should I put it
Using await for a Promise makes the function wait till the promise is resolved (here when setItem is done). You do not need to explicitly return a Promise value from the async function in this case. If you do not use await, the function will return prematurely (without waiting for the setItem promise). The setItem promise will still resolve concurrently just that your code wouldn't be able to know when it is resolved.
By using await for setItem here, you just propagate promise resolution to the calling function(here in the then(res => {...}) block).
In the then(res => {}) block you can either use await to wait for the save to complete before executing the next statement. Or use then/catch and add the next statement to execute after save is done in the then block.
Edit: As OP mentioned in the comments below, the redirection to the next component is done automatically. Well, in this case, setting the value in AsyncStorage and immediately getting it in the next component might not work as expected because of the above-mentioned reason.
First, you will need to check if the auto-redirection to the next component is really done after the axios request completes or before it, i.e. as soon as response?.type === 'success'. I am unable to understand why you have made the axios request after you already got success from auth request
If the redirection is happening before the axios request call then you might be able to access the token in the success condition itself:
if (response?.type === 'success'){
// Check if the token is available here?
console.debug(`Response = ${JSON.stringify(response)}`);
// If token is available here itself, then why is the axios request required?
// Save the token here itself...
// Use SessionStorage if required, implementation explained below in the answer
...
}
If you confirmed the above and the auto-redirection is really done after the axios request and NOT after response?.type === 'success' then:
You could use react-native-session-storage as volatile storage to set and get the token in the same session and use AsyncStorage in parallel to it to set and get the token in/from persistent memory.
So, the save function will look like this with SessionStorage:
import SessionStorage from 'react-native-session-storage';
...
const save = async (token) => {
try{
// Set token in SessionStorage as well to allow access to the value immediately
SessionStorage.setItem(`access_token`, token);
// Store token to AsyncStorage to persist it when the app closes.
await AsyncStorage.setItem('access_token', token);
}
catch(error){
console.log(error)
}
}
And getItem function will look like this:
import SessionStorage from 'react-native-session-storage';
...
const getItem = async () => {
try{
let token = await AsyncStorage.getItem('access_token');
// If the token is not yet set in Async Storage, fetch it from Session Storage
// If it's set in Async Storage, use that value
if(!token) // If it's null
token = SessionStorage.getItem('access_token');
setSpotifyAccessToken(token);
// Don't forget to clear both SessionStorage and AsyncStorage on logout!
}
catch(error){
console.log(error)
}
}
Why both storages?
AsyncStorage
-> to persist the token when the user re-opens the app.
SessionStorage
-> as an immediate way to R/W the value during the same session (gets cleared when the app closes).
Another solution:
Use ContextProvider, if your code structure allows it. Wrap the context over the next component to "listen" to token value state change from anywhere in the children components.

Vue + MSAL2.x + Azure B2C Profile Editing

First, I am not finding Vue specific examples using MSAL 2.x and we'd like to use the PKCE flow. I am having issues with the way the router guards are run before the AuthService handleResponse so I must be doing something wrong.
In my main.js I am doing this...
// Use the Auth services to secure the site
import AuthService from '#/services/AuthServices';
Vue.prototype.$auth = new AuthService()
And then in my AuthConfig.js I use this request to login:
loginRequest : {
scopes: [
"openid",
"profile",
process.env.VUE_APP_B2C_APISCOPE_READ,
process.env.VUE_APP_B2C_APISCOPE_WRITE
]
},
The docs say it should redirect to the requesting page but that is not happening. If user goes to the protected home page they are redirected to login. They login, everything is stored properly so they are actually logged in, but then they are sent back to the root redirect URL for the site, not the Home page.
When a user wants to login we just send them to the protected home page and there is a login method called in the router guard which looks like this:
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const IsAuthenticated = await Vue.prototype.$auth.isAuthenticated()
console.log(`Page changing from ${from.name} to ${to.name}, requiresAuth = ${requiresAuth}, IsAuthenticated = ${IsAuthenticated}`)
if (requiresAuth && !IsAuthenticated)
{
next(false)
console.log('STARTING LOGIN')
Vue.prototype.$auth.login()
// Tried this
// Vue.prototype.$auth.login(to.path)
} else {
next()
}
})
In AuthServices.js I have this...
// The user wants to log in
async login(nextPg) {
// Tell B2C what app they want access to and their invitation ID if they are new
if (store.getters.userEmail != null) {
aCfg.loginRequest.loginHint = store.getters.userEmail
}
aCfg.loginRequest.state = "APP=" + store.getters.appCode
if (store.getters.appointmentLink != null && store.getters.appointmentLink != '') {
aCfg.loginRequest.state += ",ID=" + store.getters.appointmentLink
}
// Tried this
// if (nextPg && nextPg != '') {
// aCfg.loginRequest.redirectUrl = process.env.VUE_APP_B2C_REDIRECT_URL + nextPg
// }
return await this.msalInst.loginRedirect(aCfg.loginRequest)
}
I tried puting a nextPg parameter in the login method and adding a redirectUrl property to the login request but that gives me an error saying it is not one of the configured redirect URLs.
Also, I'm trying to make the user experience better when using the above technologies. When you look at the MSAL2.x SPA samples I see that when returning from a Profile Edit, a user is logged out and they are required to log in again. That sounds like a poor user experience to me. Sample here: https://github.com/Azure-Samples/ms-identity-b2c-javascript-spa/blob/main/App/authRedirect.js
Do I need to just create my own profile editing page and save data using MSGraph to prevent that? Sorry for the noob questions. Ideas?
Update - My workaround which seems cheesy is to add these two methods to my AuthService.js:
storeCurrentRoute(nextPath) {
if (!nextPath) {
localStorage[STOR_NEXT_PAGE] = router.history.current.path
} else {
localStorage[STOR_NEXT_PAGE] = nextPath
}
console.log('Storing Route:', localStorage[STOR_NEXT_PAGE])
}
reEstablishRoute() {
let pth = localStorage[STOR_NEXT_PAGE]
if (!!pth && router.history.current.path != pth) {
localStorage[STOR_NEXT_PAGE] = ''
console.log(`Current path is ${router.history.current.path} and reEstablishing route to ${pth}`)
router.push({ path: pth })
}
}
I call storeCurrentRoute() first thing in the login method and then in the handleResponse() I call reEstablishRoute() when its not returning from a profileEdit or password change. Seems like I should be able to make things work without this.
Update Number Two - When returning from B2C's ProfileEdit User Flow the MSAL component is not logging me out properly. Here is my code from my handlePolicyChange() method in my AuthService:
} else if (response.idTokenClaims[clmPolicy] === aCfg.b2cPolicies.names.editProfile) {
Vue.nextTick(() => {
console.log('BACK FROM Profile Change')
Vue.prototype.$swal(
"Success!",
"Your profile has been updated.<br />Please log in again.",
"success"
).then(async () => {
this.logout()
})
})
}
:
// The user wants to log out (all accounts)
async logout() {
// Removes all sessions, need to call AAD endpoint to do full logout
store.commit('updateUserClaims', null)
store.commit('updateUserEmail', null)
let accts = await this.msalInst.getAllAccounts()
for(let i=0; i<accts.length; i++) {
const logoutRequest = {
account: accts[i],
postLogoutRedirectUri: process.env.VUE_APP_B2C_REDIRECT_URL
};
await this.msalInst.logout(logoutRequest);
}
return
}
It is working fine until the call to logout() which runs without errors but I looked in my site storage (in Chrome's debug window > Application) and it looks like MSAL did not clear out its entries like it does on my normal logouts (which always succeed). Ideas?
As part of the MSAL auth request, send a state Parameter. Base64 encode where the user left off inside this parameter. MSAL exposes extraQueryParameters which you can put a dictionary object inside and send in the auth request, put your state Key value pair into extraQueryParameters.
The state param will be returned in the callback response, use it to send the user where you need to.

FeatherJS - Get user information with hook?

So im trying out FeatherJS and i was able to register a new user, request a token and also request protected data (using Authorization in the header).
Very important: I am using HTTP Rest API only. The docs seem to point often times to the client feathers module, which i don't use.
So currently i have a super simple setup, i have a message service with a text. One before hook to process the message. Here i want to get back the user information:
module.exports = function (options = {}) {
return async context => {
const text = context.data.text
const user = context.params.user;
context.data = {
text,
userId: user._id
}
return context;
};
};
this doesn't work. In my mdb i only get back:
{
"_id": "5c35ce18523501803f6a8d8d",
"text": "123",
"createdAt": "2019-01-09T10:34:00.774Z",
"updatedAt": "2019-01-09T10:34:00.774Z",
"__v": 0
}
i've tried to add the token, that i always submit when i post a message via Authorization, like so:
module.exports = function (options = {}) {
return async context => {
const text = context.data.text
const user = context.params.user;
const token = context.params.accessToken
context.data = {
text,
userId: user._id,
tokId: token
}
return context;
};
};
but it seems like i always just get the same result back like shown above.
Any ideas how i can get the user information back of the current user by using the accessToken?
Never used FeathersJS before, so just trying to understand the ecosystem and how to approach this in FeathersJS.
Any advice is appreciated! Thanks in advance everyone!
Not quite sure what exactly went wrong, but i got it now working by just creating a new project.
Now i did recreate this project actually before and got the issue as above , but this time it somehow worked.
For anyone who wants to know the steps i did to 'fix' it:
1.Create a new folder
2. feathers generate app
3. feathers generate authentication
4. feathers generate service (name for the service: messages)
5. feathers generate hook (name: process-msg, before hook, model: messages)
6. Add this to the hook process-msg:
module.exports = function (options = {}) {
return async context => {
const user = context.params.user;
const text = context.data.text;
context.data = {
userId: user.email,
text,
dateTime: new Date().getTime()
}
return context;
};
};
Use postman, register a new account then authenticate to get the token. Save token and add it as Authoriztation Header inside Postman. You should then get also back the user email from the user that is registered, simply because of the token that was added to the Authorization Header.
Greetings!
go to authentication.js and find app.service definition. Right in there, create an after hook and add the details you want the client to receive
app.service('authentication').hooks({
before: {
...//as you currently have it
},
after: {
create: {
hook => {
// hook.result.accessToken is already provided
delete hook.params.user.password
hook.result.user = hook.params.user;
hook.result.token_type = 'Bearer';
hook.result.exp = 3600;
}
}
}
})
I hope this helps
So, if I understand it correctly, you want to get the user object from the hook?
You can just use const user = context.user;to accomplish this.

VueJS check periodically global function

I have a VueJS project where I need to check periodically a function to see if a token has expired once the user login to the app successfully and if the token has expired have to show a modal message to user.
I have my Singin.vue file that contains the following code:
....
methods: {
...mapActions(['authorize']),
submit() {
this.$validator.validateAll().then(result => {
if (result) {
this.error = null;
this.processing = true;
this.authorize(this.credentials).then(() => {
// ***********
// HERE I have to check periodically if the token has expired
// ***********
this.$router.push({name: 'home'});
}).catch(error => {
console.warn('error message', error);
this.error = error.response.data.message;
this.processing = false;
});
}
});
}
When this.authorize happens I route to home, but before that happens I need to start calling a function periodically. Then If user Logoff then I have to clear the interval.
So first, I don't know where is the best place to have this TokenExpiration function code. Does it make sense to have it in a store file?
This is my api.js store file where I have my authorize function and my logout function, does it make sense to have the tokenExpirationCheck function here also?
There are several ways of doing it, but I would probably solve this using a plugin, because timers should not be in the store, and the behavior is global to the application, so I wouldn't put it into any single component.
The pugin would have a vuex.watch on the stoere's logged-in flag. When it goes from false => true, remove the timer (if active) and if it goes from false => true, add the timer. The timer function can then call the vuex dispatch to handle the functionality.

Auth0 callback URL mismatch

I am doing LinkedIn authentication with auth0 in a react app. I have set localhost:3000/upload in callback urls in settings, hopping that after users login at localhost:3000/login, they would be redirected to localhost:3000/upload. However, I always get this error: url localhost:3000/login is not in the list of callback urls. Why would auth0 expect to return to the page where you just logged in after logging in. Shouldn't it be some different url. It just does not make sense to me.
Edit:
export default class AuthService {
constructor(clientId, domain) {
// Configure Auth0
const options = {
allowedConnections: ['linkedin'],
auth: {
params: {responseType: 'code'}
}
};
this.lock = new Auth0Lock(clientId, domain, options)
// Add callback for lock `authenticated` event
this.lock.on('authenticated', this._doAuthentication.bind(this))
// binds login functions to keep this context
this.login = this.login.bind(this)
this.loggedIn = this.loggedIn.bind(this)
}
_doAuthentication(authResult){
// Saves the user token
console.log(authResult);
this.setToken(authResult.idToken)
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
console.log('Error loading the Profile', error)
} else {
console.log(profile)
}
})
}
//....
Please ensure two things:
1). In your react app code
responseType: 'code'
2). On the Auth0 dashboard, under Settings -> Allowed Callback URLs put your callback entry (localhost:3000/upload) - which I think you have done but just in case.
Let me know if you are still having problems.
Make sure that there is no special hidden characters or space between the commas between the URLs when you paste it into the Auth0 Setting site. I didn't realise about this util I put every urls into Vim to check and see that there are such above cases
In the call to AuthProvider, make sure to use to same callback url as the one in Auth0 settings:
const uri='http://localhost:3000/upload';
<Auth0Provider
domain={domain}
clientId={clientId}
redirectUri={uri}>
To cause a redirect to a different URL after a successful authentication, you need to provide the redirectUrl to Lock, like this:
// Configure Auth0
const options = {
allowedConnections: ['linkedin'],
auth: {
responseType: 'code',
redirectUrl: 'http://localhost:3000/upload'
}
};
this.lock = new Auth0Lock(clientId, domain, options)
(Also notice that the responseType option goes under auth, not under auth.params.)
If you do the redirect, you won't reach the event handler you defined in your login page. You will need to either add an event handler in your destination page (and use responseType:token) or handle authentication results in your server code (this is what you will normally be doing if you are requesting a responseType: code).
the reason why you should set the callback Url in auth0 settings, because any one can use your client id and send request to google or linkedin, get the response to anywhere they set. but with this setting only you can access that response.
once your app is authorized to pull the data from linkedin, linkedin will send the data to where you specified. you should create a page to handle the response from Linkedin server. Let's name that page callback.js and this will be an example of response object.
accessToken: "hNuPLKTZHiE9_lnED0JIiiPNjlicRDp"
appState: null
expiresIn: 7200
idToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRXdSVUl5TURVeE4wSkJPRFZEUlRKRU1EVkZNemsxTXpNNU5VTXlNRGt6T0VWQlJqUkZRUSJ9.eyJodHRwOi8vbG9jYWxob3N0OjMwMDAvcm9sZSI6InNpdGVPd25lciIsImdpdmVuX25hbWUiOiJvbWFyIiwiZmFtaWx5X25hbWUiOiJpYm8iLCJuaWNrbmFtZSI6Im9tYXJpYm8xOTgyIiwibmFtZSI6Im9tYXIgaWJvIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250BQUFBQUkvQUFBQUFBQUFBQUEvQUNIaTNyLTEwLTEyVDIyOjU4OjAxLjgzM1oiLCJpc3MiOiJodHRwczovL3BvcnRmb2xpby15aWxtYXouYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTE0MDY0NTA2ODI2OTgwNTA5ODY3IiwiYXVkIjoiUEdVY242RjRRS21PRkJhb1k0UFdCeWpjVzIyT09vNGMiLCJpYXQiOjE1NzA5MjEwODIsImV4cCI6MTU3MDk1NzA4MiwiYXRfaGFzaCI6InN0R1l5SnJaMHNnbVYzSWNLWjlPeFEiLCJub25jZSI6InRrOV95b096enRmVThVVjFVMlVFR3IyMW5ORW5abjk4In0.TYS7mM8N2d7jEHFdWQGTSeAAUaDt4-0SMUG3LrcQ1r3xzY0RMGsUsEszj5xqk1GE0cIlFS10xCOYKsuHSwsFLomC1EbLjntjkledHtfD0MW84cMoXN6a-x-1-bNwl3lMYJ98qklTrNvTvkQJ6DWhei3hJ8rs8dnbNyCfckNVU6ptJU-9ef1DwWfHRomW5LQ6WSDRHZScW697gdgBEMU-Nd2SddyHhQe0kVh6lKdcbnskEAyCJLE07jfM40RQI_8LJouFcpoyImcXSDZlKv90tYfVDq9_TwE3GNaSz5I5snn0457oCgz0vuX0JoCUiaDuTIX7XiyXnozW_DxGMuhk4w"
idTokenPayload: {http://localhost:3000/role: "siteOwner", given_name: "me", family_name: "you", nickname: "nck", name: "nm", …}
refreshToken: null
scope: null
state: "xkEbffzXbdOYPLkXOUkrQeb0Jysbnlfy"
tokenType: "Bearer"
//THIS CODE IS FOR NEXT.JS9
//auth.js
class Auth0 {
constructor() {
this.auth0 = new auth0.WebAuth({
domain: "portfolio-ys.auth0.com",
clientID: "PGUWJQKmOFBaoY4PWByjcW22OOo4c",
redirectUri: "http://localhost:3000/callback",
responseType: "token id_token",
scope: "openid profile"
});
this.handleAuthentication = this.handleAuthentication.bind(this);
}
//there are too many methods are defined here i put only relevant ones
handleAuthentication() {
return new Promise((resolve, reject) => {
this.auth0.parseHash((err, authResult) => {
console.log(authResult);
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
resolve();
} else if (err) {
reject(err);
}
});
});
}
setSession function is where you set the cookies based on response object. I use js-cookie package to set the cookie.
setSession(authResult) {
const expiresAt = JSON.stringify(
authResult.expiresIn * 1000 + new Date().getTime()
);
Cookies.set("user", authResult.idTokenPayload);
Cookies.set("jwt", authResult.idToken);
Cookies.set("expiresAt", expiresAt);
}
}
const auth0Client = new Auth0();
export default auth0Client;
callback.js
import React from "react"
import auth0Client from "./auth0"
import {withRouter} from "next/router"
class Callback extends React.Component{
async componentDidMount(){
await auth0Client.handleAuthentication()
this.props.router.push('/')
}
render() {
return (
<h1>verifying logging data</h1>
)
}
}
export default withRouter(Callback) //this allows us to use router
I had similar issue "callback URL mismatch" and resolved it by running the application over https with a trusted certificate.
Here is a snippet from Auth0 applications settings section about callback URL, which says "Make sure to specify the protocol (https://) otherwisw the callback may fail in some cases."
If you're using the Android(Kotlin) SDK of auth0, I noticed that during runtime, the requested URL is being changed. e.g. app://{your_auth0_domain}/android/{package_name}/callback://{your_auth0_domain}/android/app://{your_auth0_domain}/android//callback
Originally URL was
app://{your_auth0_domain}/android/{package_name}/callback
and SDK is appending "://{your_auth0_domain}/android/app://{your_auth0_domain}/android//callback" this extra part.
Solution: Either put the same URL in auth0 setting dashboard as it showing in your logs
or
WebAuthProvider
.login(account)
.withScheme("app") // instead of complete URL, put only the remaining part from the URL,
.start(this, object : Callback<Credentials, AuthenticationException> {}
I hope it will definitely help android/app developer.