Saving Auth token in localStorage (React + ROR) - ruby-on-rails

After login functionality, I am saving my auth token in browser's localStorage, so that i can authenticate each action fired to the server. After login i have to refresh my browser to retrieve my token, since root component is not rerendered. is there any way to rerender index.js? I am building an electron app, so browser refresh is not an option.
in index.js
const AUTH_TOKEN = localStorage.getItem('user')
if(AUTH_TOKEN){
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
store.dispatch({type: AUTHENTICATED});
}
but this will only get rendered only the first time app loads. Store and routes are defined inside. So after login auth token will be saved in localstorage but its not updated in app. Any ideas?

You can just set the axios authorization header after saving the auth token.

So this is what i did
In Auth action
localStorage.setItem('user', response.data.auth_token);
localStorage.setItem('name', response.data.user.name);
localStorage.setItem('email', response.data.user.email);
axios.defaults.headers.common['Authorization'] = response.data.auth_token;
dispatch({ type: AUTHENTICATED });

you can manage token like there, its updated on token expire, token change
const watchToken = (delay) => {
return setTimeout(() => {
myApi.getToken().then(res => {
if (res.token) {
store.dispatch({ type: AUTHENTICATED })
} else {
store.dispatch({ type: NOT_AUTHENTICATED })
}
})
}, delay);
}
class App extends Component {
tokenManager;
constructor() {
super();
this.state = {
isReady: false
};
}
componentWillMount() {
this.tokenManager = watchToken(20000)
}
componentWillUpdate(nextProps, nextState) {
if (nextProps.TOKEN !== this.props.TOKEN) {
this.tokenManager = watchToken(20000);
}
}
componentWillUnmount() {
this.tokenManager.clearTimeout();
}
render() {
return (
<div>
</div>
);
}
}

Related

Can't tweet on twitter by using supabase user access token

I am using supabase/auth-helpers with sveltekit and using Twitter OAuth, and the sign up and login are working just fine.
I am using the twitter-api-v2 package, and it states that I can use access token as bearer token like so:
const client = new TwitterApi(MY_BEARER_TOKEN);
Here is my code:
import { variables } from '$lib/variables';
import { supabaseServerClient, withApiAuth } from '#supabase/auth-helpers-sveltekit';
import type { RequestHandler } from '#sveltejs/kit';
import { TwitterApi } from 'twitter-api-v2';
export const GET: RequestHandler<any> = async ({ locals }) =>
withApiAuth(
{
redirectTo: '/',
user: locals.user
},
async () => {
const loggedClient = new TwitterApi(locals.accessToken!, {
clientId: variables.twitter_oauth_client_id,
clientSecret: variables.twitter_oauth_secret
} as any);
const profile = await loggedClient.v2.me();
return {
body: {
data: profile as any
}
};
}
);
However, I am getting error 403 and sometimes 401. The error output reads:
Request failed with code 401
Here is a link to twitter api response codes: https://developer.twitter.com/en/support/twitter-api/error-troubleshooting

Axios interceptor changes the POST request's content type on 401 retry

I cannot figure out why Axios is changing my request's content-type on retry.
I am creating an axios instance as follows (notice global default header):
import axios, { type AxiosInstance } from "axios";
const api: AxiosInstance = axios.create({
baseURL: "https://localhost:44316/",
});
export default api;
I import this instance in various components within my vue3 app. When my token has expired and I detect a 401, I use the interceptor to refresh my token and retry the call as follows (using a wait pattern to queue multiple requests and prevent requesting multiple refresh tokens):
axios.interceptors.request.use(
(config) => {
const authStore = useAuthStore();
if (!authStore.loggedIn) {
authStore.setUserFromStorage();
if (!authStore.loggedIn) {
return config;
}
}
if (config?.headers && authStore.user.accessToken) {
config.headers = {
Authorization: `Bearer ${authStore.user.accessToken}`,
};
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
if (err.response.status === 401 && !err.config._retry) {
console.log("new token required");
err.config._retry = true;
const authStore = useAuthStore();
if (!authStore.isRefreshing) {
authStore.isRefreshing = true;
return new Promise((resolve, reject) => {
console.log("refreshing token");
axios
.post("auth/refreshToken", {
token: authStore.user?.refreshToken,
})
.then((res) => {
authStore.setUserInfo(res.data as User);
console.log("refresh token received", err.config, res.data);
resolve(axios(err.config));
})
.catch(() => {
console.log("refresh token ERROR");
authStore.logout();
})
.finally(() => {
authStore.isRefreshing = false;
});
});
} else {
// not the first request, wait for first request to finish
return new Promise((resolve, reject) => {
const intervalId = setInterval(() => {
console.log("refresh token - waiting");
if (!authStore.isRefreshing) {
clearInterval(intervalId);
console.log("refresh token - waiting resolved", err.config);
resolve(axios(err.config));
}
}, 100);
});
}
}
return Promise.reject(err);
}
);
But when axios retries the post request, it changes the content-type:
versus the original request (with content-type application/json)
I've read every post/example I could possible find with no luck, I am relatively new to axios and any guidance/examples/documentation is greatly appreciated, I'm against the wall.
To clarify, I used this pattern because it was the most complete example I was able to put together using many different sources, I would appreciate if someone had a better pattern.
Here's your problem...
config.headers = {
Authorization: `Bearer ${authStore.user.accessToken}`,
};
You're completely overwriting the headers object in your request interceptor, leaving it bereft of everything other than Authorization.
Because the replayed err.config has already serialised the request body into a string, removing the previously calculated content-type header means the client has to infer a plain string type.
What you should do instead is directly set the new header value without overwriting the entire object.
config.headers.Authorization = `Bearer ${authStore.user.accessToken}`;
See this answer for an approach to queuing requests behind an in-progress (re)authentication request that doesn't involve intervals or timeouts.

I want to merge the cognito user (mail/password) with google account user in'cognito'

I'm trying to link a user within 'Pre Signup Trigger' using adminLinkProviderForUser.
I can merge by setting the following in the parameter of adminLinkProviderForUser.
const params = {
DestinationUser: {
ProviderAttributeValue: username,
ProviderName: 'Cognito'
},
SourceUser: {
ProviderAttributeName: 'Cognito_Subject',
ProviderAttributeValue: providerUserId,
ProviderName: 'Google'
},
UserPoolId: userPoolId
}
But, I get an error when I try to merge the Cognito user (email/password) by first creating a Google account as follows.
const params = {
DestinationUser: {
ProviderAttributeValue: username,
ProviderName: 'Google'
},
SourceUser: {
ProviderAttributeName: 'Cognito_Subject',
ProviderAttributeValue: providerUserId,
ProviderName: 'Cognito'
},
UserPoolId: userPoolId
}
If I created a Google account first, is there a way to merge the cognito user (email / password) created later?
All the code currently written in Lambda. (I'm having trouble with the FIXME part.)
'use strict';
const AWS = require('aws-sdk');
const cognitoIdp = new AWS.CognitoIdentityServiceProvider();
const getUserByEmail = async (userPoolId, email) => {
const params = {
UserPoolId: userPoolId,
Filter: `email = "${email}"`
}
return cognitoIdp.listUsers(params).promise()
}
const linkProviderToUser = async (username, baseProviderName, userPoolId, providerUserName, providerName, providerUserId) => {
const params = {
DestinationUser: {
ProviderAttributeValue: username,
ProviderName: baseProviderName
},
SourceUser: {
ProviderAttributeName: providerUserName,
ProviderAttributeValue: providerUserId,
ProviderName: providerName
},
UserPoolId: userPoolId
}
const result = await (new Promise((resolve, reject) => {
cognitoIdp.adminLinkProviderForUser(params, (err, data) => {
if (err) {
reject(err)
return
}
resolve(data)
})
}))
return result
}
exports.handler = async (event, context, callback) => {
console.log(event)
if (event.triggerSource == 'PreSignUp_ExternalProvider') {
const userRs = await getUserByEmail(event.userPoolId, event.request.userAttributes.email)
if (userRs && userRs.Users.length > 0) {
let [ providerName, providerUserId ] = event.userName.split('_') // event userName example: "Facebook_12324325436"
providerName = providerName === 'google' ? 'Google' : providerName
await linkProviderToUser(userRs.Users[0].Username, 'Cognito' ,event.userPoolId, 'Cognito_Subject', providerName, providerUserId)
} else {
console.log('Users Not Found. This process skip.')
}
}
if (event.triggerSource == 'PreSignUp_SignUp') {
const userRs = await getUserByEmail(event.userPoolId, event.request.userAttributes.email)
if (userRs && userRs.Users.length > 0) {
// FIXME: This will be executed if the Cognito user is created later.I want to set parameters appropriately and merge with Google account users.
} else {
console.log('Users Not Found. This process skip.')
}
}
return callback(null, event)
}
According to the AWS documentation for the AdminLinkProviderForUser action, you cannot link a native Cognito email/password user to any existing user. Only federated users from external IdPs can be linked.
Definition for the SourceUser parameter:
An external IdP account for a user who doesn't exist yet in the user
pool. This user must be a federated user (for example, a SAML or
Facebook user), not another native user.

Meteor access tokens

When using Meteor, with the new authentication system, how can I get facebook access token, when logged with accounts-facebook package?
get it either from a Meteor.method:
// server
Meteor.methods({
getAccessToken : function() {
try {
return Meteor.user().services.facebook.accessToken;
} catch(e) {
return null;
}
}
});
// client
Meteor.call("getAccessToken", function(error, accessToken){
console.log(accessToken);
})
or publish it:
//server
Meteor.publish("currentUserAccessToken", function(){
var self = this;
if (this.userId()){
handle = Meteor.users.find(this.userId()).observe({
added: function(user){
self.set("currentUserAccessToken", user._id, {value: user.services.facebook.accessToken});
self.flush();
},
changed: function(user){
self.set("currentUserAccessToken", user._id, {value: user.services.facebook.accessToken});
self.flush();
}
});
this.onStop(function() {
handle.stop();
});
}
});
//client
var AccessToken = new Meteor.Collection("currentUserAccessToken");
Meteor.subscribe("currentUserAccessToken");
//access the value
var accessToken = AccessToken.findOne().value;
Updating and simplifying Lloyd's answer, we get this:
server
Meteor.publish("currentAccessToken", function(){
return Meteor.users.find(this.userId, {fields: {'services.facebook.accessToken': 1}});
});
client
Meteor.autosubscribe(function(){
var newUser = Meteor.user();
Meteor.subscribe('currentAccessToken');
});
It updates every time your user changes status, and access token can be accessed (when exists) by Meteor.user().services.facebook.accessToken

Silent refresh not working with OIDC-client in Angular 5

I have an issue with the silent refresh with oidc-client.
The signin works fine and I'm able to acquire a token.
However, the silent refresh doesn't fire, nothing happens. When I subscribe to methods that check token expiry (methods in subscribeevents in authservice.ts below), these methods never fire - and the method isLoggedIn() always return true even if the token has expired.
Here is my code :
import { Component, OnInit } from '#angular/core';
import { UserManager } from 'oidc-client';
import { getClientSettings } from '../openIdConnectConfig';
import { AuthService } from '../services/auth.service';
#Component({
selector: 'app-silentrefresh',
templateUrl: './silentrefresh.component.html',
styleUrls: ['./silentrefresh.component.css']
})
export class SilentRefreshComponent implements OnInit {
constructor(private _authService:AuthService) {
}
ngOnInit() {
this._authService.refreshCallBack();
}
}
Then my authservice :
import { UserManagerSettings, UserManager, User } from 'oidc-client';
import { Injectable } from '#angular/core';
import { getClientSettings } from '../openIdConnectConfig';
#Injectable()
export class AuthService {
private _manager = new UserManager(getClientSettings());
private _user: User = null;
constructor() {
this._manager.getUser().then(user => {
this._user = user;
});
this._manager.events.addUserLoaded(user => {
this._user = user;
});
this.subscribeevents();
}
public isLoggedIn(): boolean {
return this._user != null && !this._user.expired;
}
public getClaims(): any {
return this._user.profile;
}
public subscribeevents(): void {
this._manager.events.addSilentRenewError(() => {
console.log("error SilentRenew");
});
this._manager.events.addAccessTokenExpiring(() => {
console.log("access token expiring");
});
this._manager.events.addAccessTokenExpired(() => {
console.log("access token expired");
});
}
public refreshCallBack(): void {
console.log("start refresh callback");
this._manager.signinSilentCallback()
.then(data => { console.log("suucess callback") })
.catch(err => {
console.log("err callback");
});
console.log("end refresh callback");
}
getUser(): any {
return this._user;
}
getName(): any {
return this._user.profile.name;
}
getAuthorizationHeaderValue(): string {
return `${this._user.token_type} ${this._user.access_token}`;
}
startAuthentication(): Promise<void> {
return this._manager.signinRedirect();
}
completeAuthentication(): Promise<void> {
return this._manager.signinRedirectCallback().then(user => {
this._user = user;
});
}
}
And my config:
import { UserManagerSettings } from "oidc-client";
export function getClientSettings(): UserManagerSettings {
return {
authority: 'https://login.microsoftonline.com/136544d9-038e-4646-afff-10accb370679',
client_id: '257b6c36-1168-4aac-be93-6f2cd81cec43',
redirect_uri: 'http://localhost:4200/auth-callback',
//redirect_uri: 'https://demoazureadconnectangular5.azurewebsites.net/auth-callback',
post_logout_redirect_uri: 'http://localhost:4200/',
//post_logout_redirect_uri: 'https://demoazureadconnectangular5.azurewebsites.net/',
response_type: "id_token",
scope: "openid profile",
filterProtocolClaims: true,
loadUserInfo: true,
automaticSilentRenew: true,
silent_redirect_uri: 'http://localhost:4200/assets/silentrefresh',
metadata: {
issuer: "https://sts.windows.net/136544d9-038e-4646-afff-10accb370679/",
authorization_endpoint: "https://login.microsoftonline.com/136544d9-038e-4646-afff-10accb370679/oauth2/authorize",
token_endpoint: "https://login.microsoftonline.com/136544d9-038e-4646-afff-10accb370679/oauth2/token",
//jwks_uri: "https://login.microsoftonline.com/common/discovery/keys",
jwks_uri: "http://localhost:4200/assets/keys.json",
//jwks_uri: "https://demoazureadconnectangular5.azurewebsites.net/assets/keys.json",
//jwks_uri: "http://localhost:50586/api/keys",
signingKeys: [{ "ApiAccessKey": "NgixniZ0S1JHxo7GPEZYa38OBTxSA98AqJKDX5XqsJ8=" }]
}
};
}
I also tried to use a static page like this:
<head>
<title></title>
</head>
<body>
<script src="oidc-client.min.js"></script>
<script>
var usermanager = UserManager().signinSilentCallback()
.catch((err) => {
console.log(err);
});
</script>
</body>
It's never fired neither
In order to test, I've changed the ID token expiry to 10 min.
I use Azure AD Connect (Open Id Connect in Azure) and Microsoft says it's not fully compatible with Open ID Connect standard... So I don't know if it's on my side or Azure side.
Somebody can help me to solve this?
The problem is that you are not asking access_token from azure AD, only id_token. You must set response_type to id_token token to get both tokens. This change will need also few more parameters. For example resource for your backend.
I have answered similar question here. I'm using also Angular 5 and oidc client. https://stackoverflow.com/a/50922730/8081009
And I answer you here also before https://github.com/IdentityModel/oidc-client-js/issues/504#issuecomment-400056662
Here is what you need to set to get silent renew working.
includeIdTokenInSilentRenew: true
extraQueryParams: {
resource: '10282f28-36ed-4257-a853-1bf404996b18'
}
response_type: 'id_token token',
scope: 'openid'
loadUserInfo: false,
automaticSilentRenew: true,
silent_redirect_uri: `${window.location.origin}/silent-refresh.html`,
metadataUrl: 'https://login.microsoftonline.com/YOUR_TENANT_NAME.onmicrosoft.com/.well-known/openid-configuration',
signingKeys: [
add here keys from link below
]
https://login.microsoftonline.com/common/discovery/keys
I'm also using different static page for callback endpoint with silent renew because this way user won't notice a thing. This page is minimum possible so oidc won't load whole angular application to hidden iframe what it is using for silent renew. So this is recommended to be more efficient.
<head>
<title></title>
</head>
<body>
<script src="assets/oidc-client.min.js"></script>
<script>
new Oidc.UserManager().signinSilentCallback()
.catch((err) => {
console.log(err);
});
</script>
</body>
Check if you have correct redirect URI in the database.
Check you have added the following in your angular.json file:
...
"assets": [
"src/assets",
"silent-refresh.html",
"oidc-client.min.js"
.....
],
...
Check silent-refresh.html:
<script src="oidc-client.min.js"></script><script>
var mgr = new Oidc.UserManager();
mgr.signinSilentCallback().catch(error => {
console.error(error);
});
</script>
Check you do not create more than one instance of UserManager
You can do either way - automaticSilentRenew: false, or automaticSilentRenew: true, I will recommend using automaticSilentRenew: false and trigger an event on expiring.
https://github.com/IdentityModel/oidc-client-js/wiki
public renewToken() {
return this.manager.signinSilent().then(u => {
this.user = u;
}).catch(er => {
console.log(er);
});
}
this.manager.events.addAccessTokenExpiring(x => {
console.log('Acess token expiring event');
this.renewToken().then(u => {
console.log('Acess token expiring event renew success');
});
});
If the above things do not work then check the identity server code.
Identity server code
Startup
services.AddIdentityServer(options =>
{
options.Authentication.CookieLifetime = TimeSpan.FromDays(30);
options.Authentication.CookieSlidingExpiration = true;
});
services.AddAuthentication(x => x.DefaultAuthenticateScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme);
Logout
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.DefaultCookieAuthenticationScheme);
Thanks to https://github.com/IdentityModel/oidc-client-js/issues/911#issuecomment-617724445
Simplest reason can be, not adding silent renew url as a redirect url in identity server configuration.
In your identity server database, redirect urls for your clients should be like this
redirectUrls: [http://localhost:4200/assets/silentrefresh, http://localhost:4200/auth-callback]
I have used some different approach to initate the silentRenw, instead of using
automaticSilentRenew: true,
I decided to explicitly call the signInSilent();.
Reason for doing the i was facing some issues as automaticSilentRenew: true, was not working as expected.
I initialized the event and method in my UserAuth class constructor that implements my interface
constructor(private oidcSettings: CoreApi.Interfaces.Authentication.OpenIDConnectionSettings)
{
this.userManager.events.addAccessTokenExpiring(() =>
{
this.userManager.signinSilent({scope: oidcSettings.scope, response_type: oidcSettings.response_type})
.then((user: Oidc.User) =>
{
this.handleUser(user);
})
.catch((error: Error) =>
{
//Work around to handle to Iframe window timeout errors on browsers
this.userManager.getUser()
.then((user: Oidc.User) =>
{
this.handleUser(user);
});
});
});
}
Where as handleUser is just check for logged in user.
So if you initialize the signInSilent process in your constructor and then call signInSilent complete i.e. callback it may work.
Not sure what oidc-client.js version you are using, this should never have worked.
```
new Oidc.UserManager().signinSilentCallback()
.catch((err) => {
console.log(err);
});
``
Usermanager is in **Oidc** object.

Resources