silent sign in using oidc client with identity server 4 - javascript

I'm trying to implement silent login in oidc-client to use with Angular 2
How can use oidc client to silently check if user is already logged in (idsvr4) and display the login details.
the following code works, but i need to refresh the page
idsvr 4 client
// JavaScript Client
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = { "http://localhost:5002/callback.html" },
PostLogoutRedirectUris = { "http://localhost:5002/index.html" },
AllowedCorsOrigins = { "http://localhost:5002" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1",
},
RequireConsent=false,
AllowOfflineAccess = true
}
client side code
var config = {
authority: "http://localhost:5000",
client_id: "js",
redirect_uri: "http://localhost:5002/callback.html",
silent_redirect_uri: "http://localhost:5002/callback.html",
response_type: "id_token token",
scope: "openid profile api1 offline_access",
post_logout_redirect_uri: "http://localhost:5002/index.html",
// Number of seconds before the token expires to trigger
// the `tokenExpiring` event
accessTokenExpiringNotificationTime: 4,
// Do we want to renew the access token automatically when it's
// about to expire?
automaticSilentRenew: false,
// Do we want to filter OIDC protocal-specific claims from the response?
filterProtocolClaims: false,
// use localStorage
userStore: new Oidc.WebStorageStateStore({ store: window.localStorage })
};
var mgr = new Oidc.UserManager(config);
// You can hook a logger to the library.
// Conveniently, the methods exposed by the logger match
// the `console` object
Oidc.Log.logger = console;
// When a user logs in successfully or a token is renewed, the `userLoaded`
// event is fired. the `addUserLoaded` method allows to register a callback to
// that event
mgr.events.addUserLoaded(function (loadedUser) {
console.log("$$$$$$$$$$$$$$$$$$$$$$$ added");
});
// Same mechanism for when the automatic renewal of a token fails
mgr.events.addSilentRenewError(function (error) {
console.error('$$$$$$$$$$$$$$$$$$$$$$$ error while renewing the access token', error);
});
// When the automatic session management feature detects a change in
// the user session state, the `userSignedOut` event is fired.
mgr.events.addUserSignedOut(function () {
alert('The user has signed out');
});
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
}
else {
log("User not logged in");
// log("*****************************************************");
mgr.signinSilent()
.then(function (newUser) {
console.log("doneeeeeeeeeeeeeeeeeeeee");
console.log(newUser);
console.log(newUser.profile);
}).catch(function (e) {
console.log("======== " + e);
});;
mgr.signinSilentCallback().then(function (newUser) {
console.log("doneeeeeeeeeeeeeeeeeeeee");
console.log(newUser);
console.log(newUser.profile);
}).catch(function (e) {
console.log("&&&&&&&&&&&& "+e);
});
}
});
no user is getting returned in either methods of silentSignIn
I want to get if user is logged in and retrieve the information as soon as the client is open.
Or if there's a better way to do this in angular 2 then it's better.

I had the same problem. I managed to solve it by using the following signin() method and by managing the process sign in response:
function signin() {
manager.createSigninRequest().then(function (req) {
window.location = req.url;
}).catch(function (err) {
log(err);
});
}
manager.processSigninResponse().then(function (response) {
log("signin response success", response);
}).catch(function (err) {
});
manager.events.addUserLoaded(function (user) {
manager.getUser().then(function () {
log("User logged in", user.profile);
});
});
function api() {
mgr.getUser().then(function (user) {
var url = "http://localhost:5001/identity";
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, JSON.parse(xhr.responseText));
}
xhr.setRequestHeader("Authorization", "Bearer " + idToken);
xhr.send();
});
}

Related

MSAL.js infinite loop, not calling Microsoft Sign-In Page

I am trying really hard to make MSAL work with Vanilla JavaScript, but it just doesn't work.
Whenever I am trying to click the Sign In button on the Login Page, the Login page just refreshes and nothing happens, the Microsoft Sign In Page is not being shown.
Also no errors are logged to console.
After clicking the Sign In button for like 200 or more times and page refreshing that many times, the Microsoft Sign In page shows up and then it works fine.
Script reference
<script type="text/javascript" src="https://alcdn.msauth.net/lib/1.4.4/js/msal.js" integrity="sha384-fTmwCjhRA6zShZq8Ow5ZkbWwmgp8En46qW6yWpNEkp37MkV50I/V2wjzlEkQ8eWD" crossorigin="anonymous"></script>
JS MSAL code
//Config
const msalConfig = {
auth: {
clientId: "{Client ID}",
authority: "{Authority}",
redirectUri: "{Base App URL}/Dashboard", //This URL is registered in Microsoft AAD
navigateToLoginRequestUrl: false
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true,
}
};
//Login Request Object
const loginRequest = {
scopes: ["User.Read"],
prompt: 'login',
};
//Initialize MSAL
var myMSALObj = new Msal.UserAgentApplication(msalConfig);
myMSALObj.handleRedirectCallback(authRedirectCallBack);
//Handle Callback
function authRedirectCallBack(error, response) {
if (error) {
console.log(error);
} else {
if (response.tokenType === "id_token") {
console.log("id_token acquired at: " + new Date().toString());
if (myMSALObj.getAccount()) {
showWelcomeMessage(myMSALObj.getAccount());
}
} else if (response.tokenType === "access_token") {
console.log("access_token acquired at: " + new Date().toString());
accessToken = response.accessToken;
try {
callMSGraph(graphConfig.graphMailEndpoint, accessToken, updateUI);
} catch (err) {
console.log(err)
}
} else {
console.log("token type is:" + response.tokenType);
}
}
}
$(document).on("click", "#btn-testing-msal", function () {
myMSALObj.loginRedirect(loginRequest);
});
Any help will be appreciated, Thanks in advance
Debug your frontend code like this, just trying to make sure where caused your problem, and it may have some help:

Firebase: Getting new token after current FCM token expires

I am working on a reactJS website with firebase as a backend. One of the main service that I give to my user is notification everyday. I had implemented a code to ask for permission, if they grant access, store them to my database. But after sometime I noticed that tokens are expiring. I did a research about it and found out about onTokenRefresh() method. I implemented that also. But I believe for some reason, it is not working correctly. I am not getting the new tokens if tokens are expired. I'll paste the snippet below of what I am trying to do.
On the main page,
if (!("Notification" in window)) {
//alert("This browser does not support desktop notification");
}
// Check whether notification permissions have already been granted
else if (Notification.permission === "granted") {
// Refresh logic here
this.checkForRefreshToken();
}
// Otherwise, ask the user for permission
else if (Notification.permission !== "denied") {
//Ask user here, then call another function for storing token
this.addNewToken()
}
If user is accepting notification for the first time, I call addNewToken() method
addNewToken = () => {
this.auth.onAuthStateChanged(authUser => {
const messaging = firebase.messaging();
messaging
.requestPermission()
.then(() => {
return messaging.getToken();
})
.then(token => {
firebase.database().ref('tokens').once('value')
.then(snapshots => {
let tokenExist = false
snapshots.forEach(childSnapshot => {
// console.log("Child snapshot: ", childSnapshot.key);
if (childSnapshot.val().token === token) {
tokenExist = true
return console.log('Device already registered.');
}
})
if (!tokenExist) {
// console.log('Device subscribed successfully');
return firebase.database().ref('tokens').push(token);
}
})
})
.catch(error => {
if (error.code === "messaging/permission-blocked") {
// console.log("Please Unblock Notification Request Manually");
} else {
// console.log("Error Occurred", error);
}
});
})
}
Now if user has already subscribed, I am checking if onTokenRefresh() is called, basically if token needs a refresh.
checkForRefreshToken = () => {
this.auth.onAuthStateChanged(authUser => {
const messaging = firebase.messaging();
messaging.onTokenRefresh(() => {
messaging.getToken().then((refreshedToken) => {
const token = refreshedToken;
firebase.database().ref('tokens').once('value')
.then(snapshots => {
let tokenExist = false
snapshots.forEach(childSnapshot => {
if (childSnapshot.val().token === token) {
tokenExist = true
return console.log('Device already registered.');
}
})
if (!tokenExist) {
return firebase.database().ref('device_ids').push(token);
}
})
})
})
})
}
I don't know what is going wrong here, but I am not able to get the new tokens.
For testing purpose, I have my own device with expired token, I deployed this code and opened my website on the device, refreshed the page etc but I didn't get the new token.
Also, it would be great if anyone can help me how I can test expired tokens locally.
I found different methods for an app, but not for website (javascript).
Thanks
auth.onAuthStateChanged(...) will add an event listener to events signalling changes in the authentication state. In your code above, you expect that the code for addNewToken and checkForRefreshToken are evaluated immediately, but instead they are simply adding new event listeners. The same applies for onTokenRefresh().
So reworking addNewToken() into an on-demand function yields:
requestMessagingToken = () => {
let authUser = this.auth.currentUser;
if (!authUser) {
console.log("Not logged in!");
return Promise.resolve(false); // silently ignore & fail
}
const messaging = firebase.messaging();
return messaging
.requestPermission()
.then((permission) => {
if (permission !== 'granted') {
throw 'insufficient permissions'; // notification permission was denied/ignored
}
return messaging.getToken();
})
.then(token => saveMessagingTokenForUser(authUser.uid, token))
.catch(error => {
if (error && error.code === "messaging/permission-blocked") {
// console.log("Please Unblock Notification Request Manually");
} else {
// console.log("Error Occurred", error);
}
return false; // silently fail
});
};
This has the added benefit of being able to handle auth state changes easily using:
this.auth.onAuthStateChanged(requestMessagingToken);
Once you've been granted permission to send notifications, you can activate the refreshed token listener using:
const messaging = firebase.messaging();
messaging.onTokenRefresh(() => {
let authUser = this.auth.currentUser;
if (!authUser) {
console.log("Not logged in!");
return; // ignore
}
messaging.getToken().then((refreshedToken) => {
return saveMessagingTokenForUser(authUser.uid, refreshedToken);
}).catch((err) => {
console.log('Unable to retrieve/store messaging token ', err);
});
});
Lastly, to save the token to the database, instead of searching through your database for a matching device token as the value of a push ID, you can use the token itself as the key. Furthermore, to make outdated token management easier, it is best to split your tokens by user.
"userData": {
"userId1": {
"tokens": {
"deviceToken1": true,
"deviceToken2": true,
"deviceToken3": true,
},
...
},
"userId2": {
"tokens": {
"deviceToken4": true,
"deviceToken5": true
},
...
},
...
}
Using this structure, you can use transactions to check if the data has been stored in the database and use cloud functions to check for invalid tokens.
saveMessagingTokenForUser = (uid, token) => {
return firebase.database().ref('userData/' + uid + '/tokens/' + token)
.transaction((currentData) => {
if (currentData != null) {
console.log('Device already registered.');
return; // abort transaction, no changes needed
} else {
console.log('Saving token to database.');
return true;
}
})
.then(() => true); // no errors = success
};
On the server you could run a Cloud Function listening to new device tokens added to the database and check that user's other device tokens for expiry.
exports.checkUserForOutdatedTokens = functions.database.ref('/userData/{userId}/tokens/{tokenId}')
.onCreate((newTokenSnapshot, context) => {
return newTokenSnapshot.ref.once('value')
.then((tokensSnapshot) => {
let tokens = Object.keys(tokensSnapshot.val());
const message = {
data: {heartbeat: true},
tokens: tokens,
}
return admin.messaging().sendMulticast(message)
.then((response) => {
if (response.failureCount > 0) {
const dataToDelete = {};
response.responses.forEach((resp, idx) => {
if (!resp.success) {
dataToDelete[tokens[idx]] = null;
}
});
console.log('List of tokens that caused failures: ' + Object.keys(dataToDelete));
return tokensSnapshot.ref.update(dataToDelete); // deletes failed tokens
}
});
});
});

MobileServices.web.js unauthorized api call

When I leave my WinJS app dormant for a while and then come back to it, and i click on a button, for some reason my calls to my backend aren't working.
I get an "Unauthorized" error from the server.
How do I modify the invokeApi so that it reauthenticates the user or something?
Does anybody have any experience using mobileservices.web.js and how to keep the end user perpetually logged in without having to reauthenticate themselves?
Thankyou.
client.invokeApi("getTopForumsTotal", {
method: "post"
}).then(function (results) {
// do something
}, function (error) {
WinJS.log(error);
});
I use winjs mobileService to authenticate the user.
client.login("microsoftaccount").done(function (results) {
// Create a credential for the returned user.
credential = new Windows.Security.Credentials.PasswordCredential("myapp", results.userId, results.mobileServiceAuthenticationToken);
vault.add(credential);
completeDispatcher();
}, function (error) {
WinJS.log(JSON.stringify(error));
errorDispatcher(error);
});
and this is what I use to refresh the end users token.
client._request("GET", "/.auth/refresh", null, null, {
accept: "application/json",
"ZUMO-API-VERSION": "2.0.0"
}, [], (error, response) => {
if (!error) {
var userObject = JSON.parse(response.responseText)
if (userObject.authenticationToken) {
client.currentUser.mobileServiceAuthenticationToken = userObject.authenticationToken;
testCall().done(function (success) {
if (success) {
credential = new Windows.Security.Credentials.PasswordCredential("myapp", userObject.user.userId, userObject.authenticationToken);
vault.add(credential);
authenticated = true;
completeDispatcher();
}
else errorDispatcher('testCall API does not exist');
});
}
else errorDispatcher('no authentication token returned');
}
else errorDispatcher(error);
});
Instead of wrapping a promise around every API call I just incorporated an idle routine on the client that refreshes the user token when they return to the app as well as refreshes the token every 59 seconds that they are idle.
So for all intense and purposes they will always have an valid token or perpetual state.
$(document).idle({
onIdle: function () {
// refresh user token
if (User.Person !== null)
User.Person.reauthenticate().done();
},
onActive: function () {
// when the user returns refresh their token 1 more time
if (User.Person !== null)
User.Person.reauthenticate().done();
},
idle: 59000, // 59 seconds
recurIdleCall: true // will keep refreshing every 59 seconds
});
https://github.com/kidh0/jquery.idle

Why gapi.auth.authorize with "immediate: false" doesn't close popup and fire callback?

I'm working on some chrome-extension. The Google Developer Console is configured and using gapi eventually works, but I've got problem with, let's say, UX.
So here is scenario I'm trying to acheive:
Try to authenticate with Google Chrome logged in user.
If fail, try to authenticate via gapi.auth.authorize with immediate: true.
If fail, try to authenticate via gapi.auth.authorize with immediate: false.
And this kind of works. I get the popup which asks for permission, I click Accept, but then popus goes blank, title is set to "Connecting..." (doesn't close) and callback function is never fired.
I know access is granted because when I click accept and reload the page, it can authorize using immediate:true and my extension works perfectly.
I check few issues, topics and questions asking different queries in google searching for the answer and I found this sollutions:
setTimeout(checkAuth, 1) - tried, no success.
I deduced that immediate:false cannot be called right after immediate:true, so I give it a try and tried to authenticate with immediate:false as first. Same results.
I tried adding gapi.auth.init, and checkingAuth in it's callback (also using setTimeout).
So here is bit of code (background.js). Sorry it looks like spaghetti, I'm beginner in JS.
function respond(interactive, sendResponse) {
xhrWithAuth('GET',
'https://www.googleapis.com/gmail/v1/users/me/profile',
interactive, // false
onUserMailFetched, sendResponse);
function xhrWithAuth(method, url, interactive, callback, sendResponse) {
var access_token;
var retry = true;
getToken();
// 1. trying to use Chrome user
function getToken() {
chrome.identity.getAuthToken({
interactive: interactive
}, function (token) {
if (chrome.runtime.lastError) {
// 2. here lastError is User is not signed in. Calling onUserMailFetched
callback(chrome.runtime.lastError, null, null, sendResponse);
}
access_token = token;
requestStart();
});
}
// I guess not important in topic
function requestStart() {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.setRequestHeader('Authorization', 'Bearer ' + access_token);
xhr.onload = requestComplete;
xhr.send();
}
// I guess not important in topic
function requestComplete() {
if (this.status == 401 && retry) {
retry = false;
chrome.identity.removeCachedAuthToken({
token: access_token
},
checkAuth_neverCalled);
} else {
callback(null, this.status, this.response, sendResponse);
}
}
// I guess not important in topic
function checkAuth_neverCalled() {
console.log("checking auth when getAuthToken fails");
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
// Handle the result of a gapi.auth.authorize() call.
function handleAuthResult(authResult) {
console.log("authenticated: ", authResult);
if (authResult) {
// do something with data
} else {
consoel.log("failed");
}
}
}
}
// This is important part.
function onUserMailFetched(error, status, response, sendResponse) {
if (!error && status == 200) {
// do something with data
} else {
// 3. as we have error at first call, we checkAuth with immediate = true
setTimeout(function () {
checkAuthWhenNotLogged(sendResponse, true);
}, 10);
}
}
// This is important part.
function checkAuthWhenNotLogged(sendResponse, immediateVal) {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: immediateVal
}, handleAuthResult);
// Handle the result of a gapi.auth.authorize() call.
// 5. But this function is never called again (when called with false).
function handleAuthResult(authResult) {
if (authResult) {
// 4. and this is called when checkAuth with true fail. We call checkAuth (itself) with false.
if (authResult.error == "immediate_failed") {
gapi.auth.init(function () {
setTimeout(function () {
checkAuthWhenNotLogged(sendResponse, false);
}, 10);
});
} else {
// yay, we are authneticated and can call gmail service
gapi.client.load('gmail', 'v1', function () {
var request = gapi.client.gmail.users.getProfile({
'userId': 'me'
});
request.execute(function (profile) {
// do something with data
});
});
}
} else {
console.log("failed");
}
}
}
}
Any hint, link or solution will be apreciated.
Ok, here is what I did to make OAuth2 work.
Scenario looks like:
Try to authenticate with Google Chrome logged in user.
If fail, try to authenticate via gapi.auth.authorize with immediate: true.
If fail, try to use chrome.identity.launchWebAuthFlow
First of all I need to explain why launchWebAuthFlow wasn't working earlier. As I mention, I configured the Google Developers Console and created key and client id as for Chrome Application. This was wrong for launchWebAuthFlow. It should be Web Application with configured redirect URL.
In chrome extension here is how you can get redirect url:
var redirectURL = chrome.identity.getRedirectURL("suffix");
It will create something like this:
https://{appId}.chromiumapp.org/
You need to set this as redirect link in your Client ID configuration. In my case I had to change used Client ID in js code.
Here is code that works for me:
(...)
var redirectURL = chrome.identity.getRedirectURL();
var options = {
'interactive': true,
url: 'https://accounts.google.com/o/oauth2/auth?' +
'scope=profile email' +
'&response_type=token' +
'&client_id=' + OAUTH2_CLIENT_ID_WEB +
'&redirect_uri=' + redirectURL
}
chrome.identity.launchWebAuthFlow(options, function (redirectUri1) {
if (chrome.runtime.lastError) {
console.log(chrome.runtime.lastError);
} else {
// redirectUri is a link with access_token param inside, we need to extract it
var paramName = "access_token"
var results = new RegExp(paramName + '=([^&#]*)').exec(redirectUri1);
if (results == null) {
console.log("not found");
} else {
console.log(results[1] || 0);
access_token = results[1]; // here we set the token
requestStart(); // here I launch google api request
}
};
});
(...)
function requestStart() {
// url = 'https://www.googleapis.com/plus/v1/people/me'
// method = 'GET'
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.setRequestHeader('Authorization', 'Bearer ' + access_token);
xhr.onload = requestComplete;
xhr.send();
}
function requestComplete() {
if (this.status == 401 && retry) {
retry = false;
chrome.identity.removeCachedAuthToken({
token: access_token
},
checkAuth);
} else {
callback(this.status, this.response);
}
}
Hope someone will take advantage of this. I know I spent way too much time on this.

Facebook on PhoneGap/Cordova -- File protocol?

Have searched everywhere for this without luck. I'm getting this error when running PhoneGap app on device (works fine in browser):
Given URL is not allowed by the Application configuration.: One or more of the given URLs is not allowed by the App's settings. It must match the Website URL or Canvas URL, or the domain must be a subdomain of one of the App's domains.
This is because I haven't added the host to the facebook app configuration. BUT, PhoneGap/Cordova accesses the app through the file:/// protocol, so there's no domain for me to add to Facebook.
Potential options: 1) Figure out how to use a cordova native plugin (this is hard because we're using Parse), 2) Switch Cordova to use localhost instead of file:// (not sure how to do this).
I've been down this road and ultimately went with option #1. Since we're not really dealing with a website there is no domain to add. Cordova needs to use file:// I don't think there is any way around it. The trick with using the plugin is keeping the login status in sync with Parse (use Parse.FacebookUtils.logIn). He's some code that should help you out. This is how I check the login status:
try {
console.log("Trying to get FB login status");
return FB.getLoginStatus(function(response) {
var accessToken, currentView, expDate, facebookAuthData, uid, user;
console.log(response);
if (response.status === "connected") {
uid = response.authResponse.userID;
accessToken = response.authResponse.accessToken;
console.log("Logged in!");
user = Parse.User.current();
if (user != null) {
console.log("we have a user");
} else {
console.log("we don't have a user... need to login with parse");
expDate = new Date(response.authResponse.expirationTime);
facebookAuthData = {
id: response.authResponse.userID + "",
access_token: response.authResponse.accessToken,
expiration_date: expDate.toISOString()
};
Parse.FacebookUtils.logIn(facebookAuthData, {
success: function(_user) {
return console.log("Logged in with Parse!");
},
error: function(error1, error2) {
return console.log("Unable to create/login as Facebook user");
}
});
}
} else {
// not logged in to fb...
}
});
} catch (e) {
return console.log(e);
}
Here's how I handle logins:
return FB.Event.subscribe("auth.authResponseChange", function(response) {
var expDate, facebookAuthData;
if (response.status === "connected") {
console.log(response.status);
try {
expDate = new Date(response.authResponse.expirationTime);
facebookAuthData = {
id: response.authResponse.userID + "",
access_token: response.authResponse.accessToken,
expiration_date: expDate.toISOString()
};
return Parse.FacebookUtils.logIn(facebookAuthData, {
success: function(_user) {
//return window.location.hash = "loginsuccess";
},
error: function(error1, error2) {
console.log("Unable to create/login to as Facebook user");
}
});
} catch (ex) {
return console.log("parse login error " + ex);
}
} else if (response.status === "not_authorized") {
// handle not auth event
} else {
// take them home?
}
});
}

Categories

Resources