'Sign in with Intastellar' - authenticator
Web development
Tech: JS, MySQL, PHP
Jeg har udviklet en egen authentication løsning, som kan implementeres på forskellige domæner. Løsningen er udviklet i PHP og MySQL.

Beskrivelse
Med Intastellar Signin authentication har jeg udviklet en auth service for en af mine hjemmesider, intastellaracconts.com som holder styr af alt hvad authentication angår. Men hvorfor har jeg valgt at udvikle min egen authentication service, når der findes mange andre derude?
Det startede tilbage i 2018 da jeg havde 2-3 forskellige hjemmesider, og hvor jeg meget gerne vil bruge en authentication løsning samt 1 konto till alle domæner. Derfor begyndte jeg at udvikle min egen authentication service som i første omgang var simple og kun kunne bruges på en hjemmeside. Dog hurtigt var jeg interessert i hvordan "Sign in with Google" fungerede og hvordan jeg kunne bruge det samme princip for mig selv.
Jeg fandt ret hurtigt ud af at de benyttede sig postMessage metoden for at dele informationer cross-domain. Derfor begyndte jeg hurtigt i, at sætte mig ind i hvordan det fungerede og hvordan jeg kan benytte det.
PostMessage
PostMessage er en metode som gør det muligt at sende beskeder mellem vinduer/frames på tværs af domæner. Metoden er en del af HTML5 og er en sikker metode for at kommunikere mellem forskellige domæner.
Metoden er bygget op af 2 parametre, en besked og en targetOrigin. Beskeden er den data som du gerne vil sende, og targetOrigin er den URL som du gerne vil sende beskeden til.
JWT
Når en bruger logger sig ind på intastellaraccounts.com, med deres email og passkey, og han bliver verificeret uden problemer, bliver der generet en JWT.
JWT består af en header som indholder hvem er udstederen, med både navn og url. I body indholder den 2 informationer fra brugeren som er logget ind
- Brugerens ID
- Brugerens email
- Scope
Genereing af JWT Token (Server-Side)
$publichash = array(
"issuer" => "Intastellar Account",
"issuer_url" => "https://apis.intastellaraccounts.com",
"exp" => time() + 83400 * 360,
);
$data = array(
"user_id" => $newsalt['salt'],
"email" => $newsalt["email"],
"allowed_scope" => $_GET["scope"]
);
$headers = array(
"iss" => "Intastellar Account",
"iss_domain" => "apis.intastellaraccounts.com",
"exp" => time() + 83400 * 360,
);
$headers = json_encode($headers);
$data = json_encode($data);
$signing_key = "{en hemmelig nøgle}";
$signiture = hash_hmac("sha256", base64_encode($headers) . "." . base64_encode($data), $signing_key, true);
$token = base64_encode($headers) . "." . base64_encode($data) . "." . base64_encode($signiture);
$jwt = base64_encode($token);
Send token til parent (Client-Side)
window.addEventListener("DOMContentLoaded", function() {
const JWT = "[Genereret JWT]";
window.opener.postMessage(JWT, "*");
window.addEventListener("message", function(e) {
if (e.data == "iframe-token-recieved") {
window.parent.close();
}
})
});
Verifikation
Token bliver sendt via Authentication header til verificerings server, hvor den bliver decoded og verificeret.
Under verificering bliver der checket om token er for gamlet (maks 30 min.) og om issueren er den korrekte samt om signaturen er korrekt.
Client-Side Verificering & udførelsen af bruger defineret funktion fra data- attributten.
fetch("https://apis.intastellaraccounts.com/verify", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
}).then(e => e.json()).then(e => {
if (e.statusCode == 200) {
const { phone, birthday } = e.account.user[0];
e.account.user.phone = phone;
e.account.user.birthday = birthday;
delete e.account.user[0];
fn(e.account);
} else {
throw new IntastellarSolutionsSDKError(e.error);
}
})
Client-Side Verificering & vidersendelse til hjemmesiden med brugerens information
fetch("https://apis.intastellaraccounts.com/verify", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
}).then(e => e.json()).then(e => {
if (e.statusCode == 200) {
const { phone, birthday } = e.account.user[0];
e.account.user.phone = phone;
e.account.user.birthday = birthday;
delete e.account.user[0];
throw new IntastellarSolutionsSDKSuccess("We´ve successfully send user data for: " + t.name);
if (window.location.href.indexOf("?") > -1) {
const query = "?" + window.location.href.split("?")[1];
// Add the query string to the url
window.location.href = window.location.protocol + "//" + document.querySelector("[data-login_uri]").getAttribute("data-login_uri") + query + "&token=" + JSON.stringify(t);
} else {
window.location.href = window.location.protocol + "//" + document.querySelector("[data-login_uri]").getAttribute("data-login_uri") + "?token=" + JSON.stringify(t);
}
} else {
throw new IntastellarSolutionsSDKError(e.error);
}
})
Server-Side verificering
// Check if the token is valid
$issuer = "Intastellar Account";
$issuer_url = "apis.intastellaraccounts.com";
// Check if the origin is a domain & not an IP like 127.0.0.1 and remove the subdomain
// Remove the subdomain if its not a ip address
$access_id = $_SERVER['HTTP_ORIGIN'];
$tokenFromHeader = $_SERVER['HTTP_AUTHORIZATION'];
if (empty($tokenFromHeader)) {
http_response_code(401);
echo json_encode([
"account" => null,
"error" => "No token provided",
"statusCode" => 401,
"status" => "Unauthorized"
]);
exit();
}
// Decode the token & remove the Bearer
$tokenFromHeader = str_replace("Bearer ", "", $tokenFromHeader);
$encodedToken = base64_decode($tokenFromHeader);
$splitToken = explode(".", $encodedToken);
$header = $splitToken[0];
$payload = $splitToken[1];
$signature = $splitToken[2];
// Check if the signature is valid
$hash = hash_hmac("sha256", $header . '.' . $payload, "intastellaraccounts.com", true);
$hash = base64_encode($hash);
if ($hash != $signature) {
http_response_code(401);
echo json_encode([
"error" => "Invalid token",
"statusCode" => 401,
"status" => "Unauthorized"
]);
exit();
}
$header = json_decode(base64_decode($splitToken[0]), true);
$payload = json_decode(base64_decode($splitToken[1]), true);
$expires = $header["exp"];
$iss = $header["iss"];
$iss_url = $header["iss_domain"];
if ($expires < time()) {
http_response_code(401);
echo json_encode([
"account" => null,
"error" => "Token expired",
"statusCode" => 401,
"status" => "Unauthorized"
]);
exit();
}
// Remove the subdomain if its not a ip address
if ($access_id != "localhost" && !filter_var($access_id, FILTER_VALIDATE_IP)) {
$access_id = preg_replace('/^(?:https?://)?(?:www.)?/i', '', $access_id);
}
if ($iss == $issuer && $iss_url == $issuer_url) {
$salt = $payload["user_id"];
$email = $payload["email"];
$scopeFromToken = $payload["allowed_scope"];
// Getting the user from the database and sending it back to the client
$publichash = array(
"account_domain" => "my.intastellaraccounts.com",
"account_id" => (int)$newsalt["id"],
"account_url" => "https://my.intastellaraccounts.com",
"scope" => $scopeFromToken,
"user" => [
"user_id" => $newsalt['salt'],
"name" => ["firstName" => $newsalt["first_name"], "lastName" => $newsalt["last_name"]],
"username" => $newsalt["username"],
"email" => $newsalt["email"],
"lanuage" => $newsalt["language"],
$scope,
"avatar" => "https://scontent-uc-d2c-7.intastellaraccounts.com/a/s/ul/p/avtr46-img/" . $image,
"cover" => "https://scontent-uc-d2c-7.intastellaraccounts.com/a/s/ul/p/avtr46-img/" . $cover
]
);
http_response_code(200);
echo json_encode([
"account" => $publichash,
"error" => null,
"statusCode" => 200,
"status" => "OK"
]);
} else {
http_response_code(401);
echo json_encode([
"account" => null,
"error" => "Invalid token",
"statusCode" => 401,
"status" => "Unauthorized"
]);
}