Working on a project that involves a Firebase-utilizing JavaScript web app that reaches out to a PHP file carrying protected functionality.
In order to do this I get a (JWT) token by calling:
firebase.auth().currentUser.getToken(true)
The full function being:
firebase.auth().currentUser.getToken(true).then(function(idToken) {
var uid = firebase.auth().currentUser.uid;
var http = new XMLHttpRequest();
var url = "http://localhost/jwt.php";
var params = "token=" + idToken + "&uid=" + uid;
http.open("POST", url, true);
//Send the proper header information along with the request
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.onreadystatechange = function() {//Call a function when the state changes.
if(http.readyState == 4 && http.status == 200) {
alert(http.responseText);
}
}
http.send(params);
console.log("TOKEN: " + idToken);
}).catch(function(error) {
// Handle error
});
On the PHP side I'm validating the token using the lcobucci/jwt library.
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Signer\Keychain;
use Lcobucci\JWT\Signer\Rsa\Sha256;
$data = new ValidationData();
$data->setIssuer('https://securetoken.google.com/<Project ID>');
$signer = new Sha256();
$keychain = new Keychain();
if($_POST["token"]) {
$token = (new Parser())->parse((string) $_POST["token"]);
$token->getHeaders(); // Retrieves the token header
$token->getClaims(); // Retrieves the token claims
$kid = $token->getHeader('kid');
$iat = $token->getClaim('iat');
//Grab Google keys
$json_url = file_get_contents('https://www.googleapis.com/robot/v1/metadata/x509/securetoken#system.gserviceaccount.com');
$json = json_decode($json_url, true);
$public_key = $json[$kid]; // Matches kid from header to private key provided by Google
try {
$isTokenValid = $token->verify($signer, $public_key); // Verify token
} catch (Exception $e) {
$isTokenValid = false;
}
if($isTokenValid) {
echo "Valid"; // Add protected functionality here
} else {
echo "Invalid";
}
}
My question is: is this secure?
Yes, verifying the token signature like this is secure.
This will prove that the token content was not modified and signed with a key from Google.
You can learn more about JWT here: https://jwt.io/introduction/
Additionally you can validate the token
$token->validate($data);
This will validate the the issuer (iss claim) and expiration time of the token (exp claim)
https://github.com/lcobucci/jwt/blob/3.2/README.md
Related
I had this weird going on.
I'm using Jersey for API end-point, HTML and javascript front end. When I authenticate user, server will response 303 to tell the client the user is authenticated and to redirect to the said page (based on user group which the user belongs to), which I put it in Location header.
But when my server response with code 303, the client (html with pure javascript in this case) receive code of 200. But if I change the code to something else, let say 401, the client receive it correctly. This happened only if server response with code 303.
Why I return 303? I'm not pretty sure myself, thought it is the right way to do it, I might just return 200, but I try to do it the proper way as much as I know and can. But that is for another time, suggestion are welcome.
And even when I try to receive the Location from header and token from cookies, it return null. As if something happened to the response from server to client. I don't have anything that change response.
Looking at the application log, everything went fine, nothing miss behave.
It was working fine, but suddenly this weird behavior happened. I already clear browser cache, clean build and deploy the app, restart tomcat, and even restart my dev machine, nothing solve it.
I not found anything related with google. My way of code things might not the right way as I'm quite new with REST.
My javascript:
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if(xhr.status === 200) {
console.log("Login API return 200");
console.log("Token: " + xhr.getResponseHeader("Token"));
} else if (xhr.status === 303) {
console.log("Status 303 with response text " + xhr.responseText);
console.log("Status 303 with redirect header of " + xhr.getResponseHeader("redirect"));
// redirect user to the page
window.location.href = xhr.getResponseHeader("Location");
} else if (xhr.status === 401) {
console.log('Unauthorized access to server.');
document.getElementById("expired").style.display = "none";
document.getElementById("credential").style.display = "block";
} else {
console.log("Returned status is " + xhr.status);
console.log("Response text is " + xhr.responseText);
}
}
};
xhr.open('POST', URL + "/api/v1/users/login");
xhr.setRequestHeader("authorization", authHeader);
xhr.send();
My java:
#POST
#Path("/login")
#Produces(MediaType.TEXT_PLAIN)
public Response authUser(#Context HttpHeaders httpHeaders,
#Context HttpServletRequest request) {
log.debug("Server name : " + request.getServerName());
String authHeader = httpHeaders.getRequestHeader("authorization").get(0);
String encodeAuth = authHeader.substring(authHeader.indexOf(' ') + 1);
String decodeAuth = new String(Base64.getDecoder().decode(encodeAuth));
String username = decodeAuth.substring(0, decodeAuth.indexOf(':'));
String password = decodeAuth.substring(decodeAuth.indexOf(':') + 1);
User user = new User(username);
log.debug("Username : " + username);
log.debug("Password : " + password);
// Check user
if (!user.login(username,password)) {
// This is a response for the ajax for unauthorized login
log.warn("Can not authenticate user, either wrong username or password.");
// Return Unauthorized response
return Response
.status(401)
.entity("Unauthorized")
.build();
}
log.debug("Authentication success. Proceed with token and cookie creation");
// Create token
String token = TokenStore.getInstance().putToken(username);
// Get user's group
String userGroup = TokenStore.getInstance().getGroup(token);
log.debug("Token created for the user is " + token);
// Create cookie
NewCookie cookie = new NewCookie("Token",token,"/app/", request.getServerName(),"User token",1800,false);
/* Create redirect URL.
* uriInfo.getBaseUriBuilder() will return http://<host>:<port>/app/api/ which is not
* desired in this scenario, the below is to eliminate /api/ from the URI
* to get only http://<host>:<port>/app
*/
String uri = uriInfo.getBaseUriBuilder().toString();
// nthLastIndexOf(2, "/", uri) is a helper method to get the nth occurrence of a character in a string, from the last character.
int secondLast = nthLastIndexOf(2, "/", uri);
String base = uri.substring(0,secondLast);
String redirect = "";
if(null != userGroup) switch (userGroup) {
case "groupA":
redirect = base + "/pageA";
break;
case "groupB":
redirect = base + "/pageB";
break;
case "groupC":
redirect = base + "/pageC";
break;
default:
break;
}
URI re = null;
log.debug("Created URI for redirect is " + redirect);
try {
re = new URI(redirect);
} catch (URISyntaxException syntax) {
log.error("Cannot convert string to URI", syntax);
} catch (Exception e) {
log.error("Cannot convert string to URI", e);
}
log.debug("Return response 303.");
// Return redirect response
return Response
.status(303)
.entity(token)
.header("Location", redirect)
.cookie(cookie)
.build();
}
A web application in Django with React components currently has been tested and works on desktop Google Chrome, Microsoft Edge, mobile Firefox and mobile Brave browsers. Unfortunately, it produces errors on Google Chrome on mobile. The React components do not seem to recognize that there is a user logged in.
The CSRF token is transferred from Django to the React components using a cookie (similarly to the process suggested at: https://django.readthedocs.io/en/stable/ref/csrf.html).
// Set what happens when the database is called by React
// Define how to get a cookie (such as a CSRF token)
export function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// Define generically what happens when the database is called by React
export function backendLookup(method, endpoint, callback, data) {
// Convert the data being sent to the database to JSON form
let jsonData;
if (data){
jsonData = JSON.stringify(data)
}
// Prepare a new request to a database
const xhr = new XMLHttpRequest()
// Prepare the URL where the database is accessible (the API endpoint)
const url = `https://artformist.com/api${endpoint}`
// Identify that the data that will be receievd is in JSON format
xhr.responseType = "json"
// Prepare the CSRF security token
const csrftoken = getCookie('csrftoken');
// Access the database, using the endpoint and method (such as "POST" or "GET") in use
xhr.open(method, url)
// Set the request header, for security purposes
xhr.setRequestHeader("Content-Type", "application/json")
// If there is a CSRF token...
if (csrftoken){
// Set the request headers
// xhr.setRequestHeader("HTTP_X_REQUESTED_WITH", "XMLHttpRequest")
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
// Set what happens immediately when a request is made
xhr.onload = function() {
// If the status returned is a 403 error...
if (xhr.status === 403) {
// Get the details of the response...
const detail = xhr.response.detail
// If the response is that the user wasn't logged in...
if (detail === "Authentication credentials were not provided."){
if (window.location.href.indexOf("login") === -1) {
// Redirect the user to the login screen TURNED OFF FOR TESTING
// window.location.href = "/login?showLoginRequired=true&status=403"
}
alert(xhr.status)
}
}
// // If the response is that something has gone wrong with the server
if (xhr.status === 500) {
// If the user isn't at a login screen already...
if (window.location.href.indexOf("login") === -1) {
// Redirect the user to the login screen. TURNED OFF FOR TESTING
// window.location.href = "/login?showLoginRequired=true&status=500"
alert(xhr.status)
}
}
// Manage the request response and status
callback(xhr.response, xhr.status)
}
// If there is an error when the call to the database is made
xhr.onerror = function (e) {
// Manage that there was a 400 error
callback({"message": "The request was an error"}, 400)
}
// Send the JSON data
xhr.send(jsonData)
}
On Chrome mobile, the site has been producing 403 or 500 error depending on what the component is attempting to do.
Here are some of the production settings. The SameSite settings have been changed and the Django version has been upgraded.
CORS_REPLACE_HTTPS_REFERER = True
HOST_SCHEME = "https://"
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Set whether requests can be made from different sites
CSRF_COOKIE_SAMESITE = None
SESSION_COOKIE_SAMESITE = None
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_SECONDS = 1000000
SECURE_FRAME_DENY = True
I have a pure Javascript app which attempts to get an access token from Azure using OAuth Authorization Flow with PKCE.
The app is not hosted in Azure. I only use Azure as an OAuth Authorization Server.
//Based on: https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead
var config = {
client_id: "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx",
redirect_uri: "http://localhost:8080/",
authorization_endpoint: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize",
token_endpoint: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token",
requested_scopes: "openid api://{tenant-id}/user_impersonation"
};
// PKCE HELPER FUNCTIONS
// Generate a secure random string using the browser crypto functions
function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
const hashed = await sha256(v);
return base64urlencode(hashed);
}
// Parse a query string into an object
function parseQueryString(string) {
if (string == "") { return {}; }
var segments = string.split("&").map(s => s.split("="));
var queryString = {};
segments.forEach(s => queryString[s[0]] = s[1]);
return queryString;
}
// Make a POST request and parse the response as JSON
function sendPostRequest(url, params, success, error) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
request.onload = function () {
var body = {};
try {
body = JSON.parse(request.response);
} catch (e) { }
if (request.status == 200) {
success(request, body);
} else {
error(request, body);
}
}
request.onerror = function () {
error(request, {});
}
var body = Object.keys(params).map(key => key + '=' + params[key]).join('&');
request.send(body);
}
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = 'Hello'+ 'webpack';
element.classList.add('hello');
return element;
}
(async function () {
document.body.appendChild(component());
const isAuthenticating = JSON.parse(window.localStorage.getItem('IsAuthenticating'));
console.log('init -> isAuthenticating', isAuthenticating);
if (!isAuthenticating) {
window.localStorage.setItem('IsAuthenticating', JSON.stringify(true));
// Create and store a random "state" value
var state = generateRandomString();
localStorage.setItem("pkce_state", state);
// Create and store a new PKCE code_verifier (the plaintext random secret)
var code_verifier = generateRandomString();
localStorage.setItem("pkce_code_verifier", code_verifier);
// Hash and base64-urlencode the secret to use as the challenge
var code_challenge = await pkceChallengeFromVerifier(code_verifier);
// Build the authorization URL
var url = config.authorization_endpoint
+ "?response_type=code"
+ "&client_id=" + encodeURIComponent(config.client_id)
+ "&state=" + encodeURIComponent(state)
+ "&scope=" + encodeURIComponent(config.requested_scopes)
+ "&redirect_uri=" + encodeURIComponent(config.redirect_uri)
+ "&code_challenge=" + encodeURIComponent(code_challenge)
+ "&code_challenge_method=S256"
;
// Redirect to the authorization server
window.location = url;
} else {
// Handle the redirect back from the authorization server and
// get an access token from the token endpoint
var q = parseQueryString(window.location.search.substring(1));
console.log('queryString', q);
// Check if the server returned an error string
if (q.error) {
alert("Error returned from authorization server: " + q.error);
document.getElementById("error_details").innerText = q.error + "\n\n" + q.error_description;
document.getElementById("error").classList = "";
}
// If the server returned an authorization code, attempt to exchange it for an access token
if (q.code) {
// Verify state matches what we set at the beginning
if (localStorage.getItem("pkce_state") != q.state) {
alert("Invalid state");
} else {
// Exchange the authorization code for an access token
// !!!!!!! This POST fails because of CORS policy.
sendPostRequest(config.token_endpoint, {
grant_type: "authorization_code",
code: q.code,
client_id: config.client_id,
redirect_uri: config.redirect_uri,
code_verifier: localStorage.getItem("pkce_code_verifier")
}, function (request, body) {
// Initialize your application now that you have an access token.
// Here we just display it in the browser.
document.getElementById("access_token").innerText = body.access_token;
document.getElementById("start").classList = "hidden";
document.getElementById("token").classList = "";
// Replace the history entry to remove the auth code from the browser address bar
window.history.replaceState({}, null, "/");
}, function (request, error) {
// This could be an error response from the OAuth server, or an error because the
// request failed such as if the OAuth server doesn't allow CORS requests
document.getElementById("error_details").innerText = error.error + "\n\n" + error.error_description;
document.getElementById("error").classList = "";
});
}
// Clean these up since we don't need them anymore
localStorage.removeItem("pkce_state");
localStorage.removeItem("pkce_code_verifier");
}
}
}());
In Azure I only have an App registration (not an app service).
Azure App Registration
The first step to get the authorization code works.
But the POST to get the access token fails. (picture from here)
OAuth Authorization Code Flow with PKCE
Access to XMLHttpRequest at
'https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token' from
origin 'http://localhost:8080' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Where in Azure do I configure the CORS policy for an App Registration?
Okay, after days of banging my head against the stupidity of Azure's implementation I stumbled upon a little hidden nugget of information here: https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser#prerequisites
If you change the type of the redirectUri in the manifest from 'Web' to 'Spa' it gives me back an access token! We're in business!
It breaks the UI in Azure, but so be it.
You should define the internal url with your local host address.
https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/application-proxy-understand-cors-issues
When I first posted, the Azure AD token endpoint did not allow CORS requests from browsers to the token endpoint, but it does now. Some Azure AD peculiarities around scopes and token validation are explained in these posts and code in case useful:
Code Sample
Blog Post
I am trying to achieve that when I call the JS function, a post request is send. In my browser I would send:
http://myuser:password#hc2:80/api/callAction?deviceID=185&name=turnOn
This works. Yet in my code it doesn't.
Important to note:
- Chrome does raise an Error: Request doesn't pass access control. If I disable this in Chrome, I doesn't display this error (yet no response from the server either).
<script type="text/javascript">
function changestate() {
var http = new XMLHttpRequest();
http.withCredentials = true;
var user = "bassie"
var pass = "password"
var url = "http://hc2/api/callAction";
var params = "deviceID=185&name=turnOff";
http.open("POST", url, true);
http.setRequestHeader("Authorization", "Basic " + user + ":" + pass);
//Send the proper header information along with the request
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
alert(http.responseText);
http.onreadystatechange = function() {//Call a function when the state changes.
if(http.readyState == 4 && http.status == 200) {
alert(http.responseText);
}
}
http.send(params);
}
</script>
The equivalent to putting the URL in the browser's location is a GET request, not POST.
Since you're sending a cross-domain request, you won't be able to read the response (unless you relay through a proxy on your origin server). So you can't read http.responseText, and can simply omit the onreadystatechange function; you'll just have to assume it
function changestate() {
var http = new XMLHttpRequest();
http.withCredentials = true;
var user = "bassie"
var pass = "password"
var url = "http://hc2/api/callAction";
var params = "deviceID=185&name=turnOff";
http.open("GET", url + "?" + params, true);
http.setRequestHeader("Authorization", "Basic " + user + ":" + pass);
http.send();
}
Eventually ended up creating a sort of like proxy. This was the main component. Not in the example (My script gets the HTTP requested) and gets the output. Below the gist of it:
req = urllib.request.Request('http://hc2:80/api/callAction?param1=1¶m2=2')
credentials = ('%s:%s' % ('user', 'password'))
encoded_credentials = base64.b64encode(credentials.encode('ascii'))
req.add_header('Authorization', 'Basic %s' %
encoded_credentials.decode("ascii"))
response = urllib.request.urlopen(req)
I'm not sure why this is happening but I will try to explain as much as I already know.
I have a website that allows you to log in with an account stored on a database. Each user can update their own set of data that is also in the same database.
When a user changes data Javascript will post an XMLHttpRequest to a php file on the server. The data is JSON encoded and is decoded in the php file and then stores the data on the database.
The problem is whenever I log into a specific account no data is sent. The string is empty after the request is sent. The post works and the php file runs but no data is present. Here is my code for sending the request in JS:
function sendXMLHttpRequest(data, phpfile){
var xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
}
else if (window.ActiveXObject) {
xhr = new ActiveXObject("Msxml2.XMLHTTP");
}
else {
throw new Error("Ajax is not supported by this browser");
}
xhr.open('POST', phpfile, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
if (this.responseText !== null)
document.getElementById('saveresponse').innerHTML = xhr.responseText;
else
alert("Ajax error: No data received");
}else alert("Ajax error: " + this.status);
}
};
xhr.send(data);
}
On the php side:
session_start();
$mysql = new Mysql();
$data = json_decode($_POST['stringData']);
echo 'Data: ' . $data . "<br />";
Normally when I echo the data is returns Array which is what I want but it doesn't echo anything when ever I log into this one specific account, the data sent is just a string where stringData is a JSON. Is there a way to see if anything IS stored there? Also if nothing is being sent why could this be? Any suggestions for my code?
Make sure the request doesn't get cached by adding a random parameter:
querystring = 'validate=' + validate+ "&rand=" + new Date().getTime();