This example demonstrates how to use Auth0 with react-admin. It is working as expected.
We are trying to adjust it so it will use the hasura data provider. We've created a new file dataProvider.js that will construct the data-provider:
import buildHasuraProvider from "ra-data-hasura";
import { ApolloClient, InMemoryCache } from "#apollo/client";
export const initDataProvider = async (token) => {
const client = new ApolloClient({
uri: process.env.REACT_APP_GRAPHQL_URI,
headers: {
Authorization: `Bearer ${token}`,
},
cache: new InMemoryCache(),
});
const dataProvider = await buildHasuraProvider({ client });
return dataProvider;
};
However, we are missing the JWT token which is created as part of the Auth0 authentication process. We do not know how to get the token in order to initialize the data provider with it. Does react-admin know how to do it on its own? if not, how do we access the JWT token to do it ourselves manually?
This is the authProvider source-code:
import authConfig from "./authConfig";
import {Auth0Client} from '#auth0/auth0-spa-js';
const auth0 = new Auth0Client({
domain: authConfig.domain,
client_id: authConfig.clientID,
redirect_uri: authConfig.redirectURI,
cacheLocation: 'localstorage',
useRefreshTokens: true
});
export default {
// called when the user attempts to log in
login: (url) => {
if (typeof url === 'undefined') {
return auth0.loginWithRedirect()
}
return auth0.handleRedirectCallback(url.location);
},
// called when the user clicks on the logout button
logout: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) { // need to check for this as react-admin calls logout in case checkAuth failed
return auth0.logout({
redirect_uri: window.location.origin,
federated: true // have to be enabled to invalidate refresh token
});
}
return Promise.resolve()
})
},
// called when the API returns an error
checkError: ({status}) => {
if (status === 401 || status === 403) {
return Promise.reject();
}
return Promise.resolve();
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) {
return Promise.resolve();
}
return auth0.getTokenSilently()
})
},
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => {
return Promise.resolve()
},
};
It is unclear to us if there is a point where we can extract the token from.
getTokenSilently should give you back the token.
You'll have to structure your React app such that you have access to the result of this method before you construct your data provider.
Related
New to the supabase universe. Simple questions
Is there a way to setup middleware in supabase?. Can Supabase fulfill this?
Add business logic middleware when creating an entity
Add special validations (ie: validate a product has stock before purchase)
Restrict information depending on user roles (ie: admins can read additional entity attributes, but not common users).
Thanks
this is now solvable using Supabase's Edge Functions: https://supabase.com/docs/guides/functions
There is an example here to solve "Restrict information depending on user roles" using Postgres' Row Level Security:
https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts
/ Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { serve } from 'https://deno.land/std#0.131.0/http/server.ts'
import { supabaseClient } from '../_shared/supabaseClient.ts'
import { corsHeaders } from '../_shared/cors.ts'
console.log(`Function "select-from-table-with-auth-rls" up and running!`)
serve(async (req: Request) => {
// This is needed if you're planning to invoke your function from a browser.
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// Set the Auth context of the user that called the function.
// This way your row-level-security (RLS) policies are applied.
supabaseClient.auth.setAuth(req.headers.get('Authorization')!.replace('Bearer ', ''))
const { data, error } = await supabaseClient.from('users').select('*')
console.log({ data, error })
return new Response(JSON.stringify({ data, error }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
})
I use firebase and AngularFireAuthGuard to protect specific routes, so that only authenticated users are allowed to access them.
In particular, my MainComponent and MgmtComponent should only be accessible to AUTHENTICATED users.
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['/login']);
const routes: Routes = [
{ path: 'teams/:teamId/sessions/:sessionId',
component: MainComponent,
canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectUnauthorizedToLogin }
},
{ path: 'mgmt',
component: MgmtComponent,
canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectUnauthorizedToLogin }
},
{
path: 'login',
component: LoginComponent
}
];
My Problem is, that the user is not redirected back to the originally requested URL, after a successful login.
So what I want/expect is:
user goes to /mgmt
as the user is not authenticated he is automatically redirected to /login
user authenticates (e.g. via google or Facebook OAuth)
user is automatically redirected back to the originally requested page (/mgmt)
Steps 1-3 work fine, but step 4 is missing.
Now that the feature request is in, you can do this using the auth guard. However, the docs are unclear, so here is how I did it.
/** add redirect URL to login */
const redirectUnauthorizedToLogin = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
return redirectUnauthorizedTo(`/login?redirectTo=${state.url}`);
};
/** Uses the redirectTo query parameter if available to redirect logged in users, or defaults to '/' */
const redirectLoggedInToPreviousPage = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
let redirectUrl = '/';
try {
const redirectToUrl = new URL(state.url, location.origin);
const params = new URLSearchParams(redirectToUrl.search);
redirectUrl = params.get('redirectTo') || '/';
} catch (err) {
// invalid URL
}
return redirectLoggedInTo(redirectUrl);
};
This is an open feature request, the angularfire team is working on it: https://github.com/angular/angularfire/pull/2448
Meanwhile I found this workaround:
In the app-routing-module.ts instead of
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['/login']);
I use following to store the url in the sessionStorage:
const redirectUnauthorizedToLogin = (route: ActivatedRouteSnapshot) => {
const path = route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/');
return pipe(
loggedIn,
tap((isLoggedIn) => {
if (!isLoggedIn) {
console.log('Saving afterLogin path', path);
sessionStorage.setItem('afterLogin', path);
}
}),
map(loggedIn => loggedIn || ['/login'])
);
};
In the LoginComponent I read the value from the session storage to redirect:
sessionStorage.getItem('afterLogin');
this.router.navigateByUrl(redirectUrl);
I've been working on an app and only realized this issue when I started to clear the cache, but my app only works fine on refresh. When I clear all the cache, refresh then run through my app, I realized that my queries were returning my custom error "GraphQL error: Not authenticated as user".
I believe something is wrong with the way that I've set up my apollo client. It seems that the context is being set as soon as it's instantiated and then never changes the context even if the token exists. It would also explain why after logging in then refreshing, the queries work with the token until the local storage/cache is cleared. So my question is what's wrong with what I have?
import { persistCache } from "apollo-cache-persist";
import { ISLOGGEDIN_QUERY } from "./components/gql/Queries"
const cache = new InMemoryCache();
const token = localStorage.getItem('token')
persistCache({
cache,
storage: localStorage
})
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
cache,
resolvers: {
Mutation: {
changeValue: (_, args, { cache }) => {
const { isAuth } = token ? cache.readQuery({ query: ISLOGGEDIN_QUERY }) : false;
cache.writeData({
data: { isAuth: !isAuth }
})
return null;
}
}
},
request: (operation) => {
operation.setContext({
headers: {
authorization: token ? token : ''
}
})
},
});
//set default values
client.cache.writeData({ data: { isAuth: token ? true : false } })
export default client;```
I know I'm a bit late but I was having this problem too and found these
https://www.apollographql.com/docs/react/networking/authentication/#reset-store-on-logout
https://stackoverflow.com/a/65204972/13491532
You can just call clear store after your login mutation
import { useApolloClient } from "#apollo/client";
const client = useApolloClient();
client.clearStore();
I made a register page that use restClient to send a POST to /users api.
But my problem is that the only way to send a POST is to be logged first as I receive this error log from the restClient :
'Could not find stored JWT and no authentication strategy was given'
Is there a way to desactivate the authentication middleware for a specific api call ?
// registerActions.js
import { CREATE } from 'admin-on-rest'
export const USER_REGISTER = 'AOR/USER_REGISTER'
export const USER_REGISTER_LOADING = 'AOR/USER_REGISTER_LOADING'
export const USER_REGISTER_FAILURE = 'AOR/USER_REGISTER_FAILURE'
export const USER_REGISTER_SUCCESS = 'AOR/USER_REGISTER_SUCCESS'
export const userRegister = (data, basePath) => ({
type: USER_REGISTER,
payload: { data: { email: data.username, ...data } },
meta: { resource: 'users', fetch: CREATE, auth: true },
})
//registerSaga.js
import { put, takeEvery, all } from 'redux-saga/effects'
import { push } from 'react-router-redux'
import { showNotification } from 'admin-on-rest'
import {
USER_REGISTER,
USER_REGISTER_LOADING,
USER_REGISTER_SUCCESS,
USER_REGISTER_FAILURE
} from './registerActions'
function* registerSuccess() {
yield put(showNotification('Register approved'))
yield put(push('/'))
}
function* registerFailure({ error }) {
yield put(showNotification('Error: register not approved', 'warning'))
console.error(error)
}
export default function* commentSaga() {
yield all([
takeEvery(USER_REGISTER_SUCCESS, registerSuccess),
takeEvery(USER_REGISTER_FAILURE, registerFailure),
])
}
You'll probably have to make your own feathers client and explicitly bypass the call to authenticate for this specific request
You can also write a rest wrappper this will intercept the call for this particular case and bypass auth
https://marmelab.com/admin-on-rest/RestClients.html#decorating-your-rest-client-example-of-file-upload
So something like below
const restWrapper = requestHandler => (type, resource, params) => {
import { fetchUtils } from 'admin-on-rest';
if (type === 'CREATE' && resource === 'users') {
return fetchUtils.fetchJson(url, params)
.then((response) => {
const {json} = response;
return { data: json };
})
}
Eliminates the need of rewriting an entire Rest Client when you only want to override the default behaviour for a single case
I have a trouble with Ember Simple Auth.
I'm trying to connect my server-side application, which working on Django 1.9 with DRF, and client-side which working on Ember 2.2.
On server side I'm obtaining token on 'http://localhost:8000/api-token-auth/'. Function requires two args from request: "username" and "password". But Ember Simple Auth send POST request with args: "username[identification]" and "password[password]", and server returns "400". I think that problem with arguments keys.
POST request
Responce
I tried to change .authenticate method in oauth2-password-grant.js(i can't write custom authenticator because i'm newbee in javascript), but nothing changed.
Manually POST request returns expected answer.
Please tell me the way to solve this problem.
And please forgive me for my english.
authenticate(identification, password, scope = []) {
return new RSVP.Promise((resolve, reject) => {
const data = { 'grant_type': 'password', username: identification, password };
const serverTokenEndpoint = this.get('serverTokenEndpoint');
const scopesString = Ember.makeArray(scope).join(' ');
if (!Ember.isEmpty(scopesString)) {
data.scope = scopesString;
}
this.makeRequest(serverTokenEndpoint, data).then((response) => {
run(() => {
const expiresAt = this._absolutizeExpirationTime(response['expires_in']);
this._scheduleAccessTokenRefresh(response['expires_in'], expiresAt, response['refresh_token']);
if (!isEmpty(expiresAt)) {
response = Ember.merge(response, { 'expires_at': expiresAt });
}
resolve(response);
});
}, (xhr) => {
run(null, reject, xhr.responseJSON || xhr.responseText);
});
});
},
My variant:
const data = { 'grant_type': 'password', 'username': identification, 'password': password };
authenticate: function () {
// var username = this.getProperties('username');
// var password = this.getProperties('password');
const {username, password} = this.getProperties('username', 'password');
this.get('session').authenticate('authenticator:oauth2', username, password).catch((reason) => {
this.set('errorMessage', reason.error || reason);
});
}
It was my mistake.