Server login makes it possible to authenticate machines instead of real users, it is mainly aimed at operations on the back end side. It is available on server -and IoT- oriented Webcom SDK (namely Java/Scala and Node.js), as well as on pure REST API (therefore usable from any HTTP-capable language).
In order to sign in using the server authentication method, you (as a developer) have only to:
- provide an RSA public key through the "authentication" tab of the Webcom developer console,
- use the corresponding RSA private key to sign a "JSON Web Token" (or JWT, as specified in RFC7519) containing specific claims,
- perform the authentication request using this JWT, and collect the resulting usual Webcom authentication token.
With the Webcom SDK for JavaScript (Node.js), the step #2 is not needed, so that you just have to provide an RSA key while performing the authentication.
Dealing with RSA keys
There are many ways to create RSA public/private key pairs, and the Webcom developer console provides some kind of generation tool
within the "authentication" tab. The generated pair is then what we call "PEM formatted", you know, the big chunk of
text between -----BEGIN PUBLIC KEY-----
and -----END PUBLIC KEY-----
.
For convenience, a popup window allows you to copy/paste them wherever you want.
It is worth mentioning that, for security reasons, generated keys neither leave the client browser, nor "travel" across any network! Keep in mind that, on your own, you must keep the private key SECRET and SAFE (just like some ring in a land called "Middle Earth"...).
The public key, however, will be stored on the Webcom back end within the settings of the "server" authentication method.
RSA keys may alternatively be provided as "JSON Web Keys" (or JWK, as specified in
RFC7517). In this case, however, you have to generate them by yourself...
Hopefully, there are many libraries available to deal with them.
The main advantage of such keys is the ability to provide them with a key ID (stored in their optional kid
field)
and therefore easily "recognize" one among many.
Any RSA key must be provided to Webcom with the following additional settings:
- a unique and mandatory name (although it is possible to use the same name within distinct applications, it is
a good idea to keep names unique across all applications),
In case of a JWK with a defined key ID, the name must equal this key ID.
- an optional description, also known as "display name",
- a mandatory boolean flag, which indicates whether the key grants full access to the application data. If so, this
means Webcom authentication tokens resulting from this key by-pass security rules.
If full access is not granted, security rules may rely on the key name to control read and write rights.
In the Webcom developer console, the text area provided for pasting public key accepts the following formats:
- PEM string, including
-----BEGIN PUBLIC KEY-----
and-----END PUBLIC KEY-----
, - JWK formatted as a JSON object, meaning something like
{"kid": "myKeyId", "kty": "RSA", ...}
, - JWK formatted as a string, meaning something like
"{\"kid\":\"myKeyId\",\"kty\":\"RSA\", ...}"
(note the leading and trailing"
).
1. Generate an RSA key pair (code example)
Here is an example of how to generate JSON Web RSA keys (JWK) with the Nimbus JOSE + JWT library:
Node.JS provides a convenient crypto
library to easily generate a key
const crypto = require('crypto');
crypto.generateKeyPair('rsa', // type of key
{
modulusLength: 2048, // use at least 2048 for key size
publicKeyEncoding: {type: 'pkcs1', format: 'pem'}, // use pem format
privateKeyEncoding: {type: 'pkcs1', format: 'pem'} // use pem format
},
(err, publicKey, privateKey) => { // callback invoked when the key is generated
console.log("public key:\n", publicKey);
console.log("private key:\n", privateKey);
}
);
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator
import com.nimbusds.jose.jwk.RSAKey
// use an RSA key generator
val rsaGen: RSAKeyGenerator = new RSAKeyGenerator(2048) // use at least 2048 for key size
// define an optional but recommended key ID
rsaGen.keyID("myKeyId")
// obtain the RSA key
val rsaJWK: RSAKey = rsaGen.generate()
// public key as a JSON object represented by a String, i.e. "{\"kid\":\"myKeyId\",\"kty\":\"RSA\", ...}"
val publicKey: String = rsaJWK.toPublicJWK.toJSONString
// entire key, meaning public and private parts of the key
val entireKey: String = rsaJWK.toJSONString
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jose.jwk.RSAKey;
// use an RSA key generator
RSAKeyGenerator rsaGen = new RSAKeyGenerator(2048); // use at least 2048 for key size
// define an optional but recommended key ID
rsaGen.keyID("myKeyId");
// obtain an RSA key
RSAKey rsaJWK = rsaGen.generate();
// public key as a JSON object represented by a String, i.e. "{\"kid\":\"myKeyId\",\"kty\":\"RSA\", ...}"
String publicKey = rsaJWK.toPublicJWK().toJSONString();
// entire key, meaning public and private parts of the key
String entireKey = rsaJWK.toJSONString();
2. Forging the JWT
Once an RSA public key is stored in Webcom, you have to forge a JWT prior to be able to authenticate on the Webcom back end. This JWT must be signed with the corresponding private key and contain the following two claims:
- an optional
id
claim that, if any, must contain the name of the public key, - a mandatory
timestamp
claim containing the amount of milliseconds since Epoch.
- an empty
id
claim will be parsed as is and therefore lead to an error (set it tonull
if you don't want it),timestamp
is a security claim that prevents "replay" attacks. A tolerance (currently set to 5 minutes) applies on the offset between the given timestamp and the actual one of the back end to validate the JWT.
Testing
For testing purpose, you can use a convenient tool online to forge and sign JWTs:
- choose
RS512
in the "Algorithm" drop down menu, - define claims in the "PAYLOAD" area. You can also use https://www.epochconverter.com to retrieve the current time in seconds and convert it into milliseconds by adding three trailing zeros. For example:
{
"id": "myKeyId",
"timestamp": 1601020899000
}
- paste RSA keys in both "VERIFY SIGNATURE" text areas (the tool accepts PEM and JWK formats),
- then collect the encoded JWT on the left side of the page...
Code example
Here is an example of how to generate a JWT with the Nimbus JOSE + JWT library:
The Webcom SDK for JavaScript (Node.js) automatically carries out the forgery of the JWT!
Nothing to do 😜
import com.nimbusds.jose.crypto.RSASSASigner
import com.nimbusds.jose.jwk._
import com.nimbusds.jose._
import com.nimbusds.jwt._
// in case of a PEM key, first retrieve its JSON representation
// CAUTION: needs org.bouncycastle:bcpkix-jdk15on and org.bouncycastle:bcprov-jdk15on libraries
val jsonString: String = JWK.parseFromPEMEncodedObjects(pemString).toJSONString
// we retrieve the RSA key from its JSON representation
val rsaJWK: RSAKey = RSAKey.parse(jsonString)
// does it have a key ID ?
val keyIdOpt: Option[String] = Option(rsaJWK.getKeyID)
// Create RSA-signer with the private key
val signer: RSASSASigner = new RSASSASigner(rsaJWK)
// Prepare JWT with claims set
val claimsSetBuilder: JWTClaimsSet.Builder = new JWTClaimsSet.Builder()
// add optional "id" claim
keyIdOpt.foreach { keyId => claimsSetBuilder.claim("id", keyId) }
// add "timestamp" claim
claimsSetBuilder.claim("timestamp", System.currentTimeMillis)
// prepare JWT with headers
val headerBuilder: JWSHeader.Builder = new JWSHeader.Builder(JWSAlgorithm.RS512)
// add optional key ID header
keyIdOpt.foreach { keyId => headerBuilder.keyID(keyId) }
// create signed JWT
val signedJWT: SignedJWT = new SignedJWT(headerBuilder.build, claimsSet.build)
// Compute the RSA signature
signedJWT.sign(signer)
// Serialize to compact format consisting of Base64URL-encoded parts delimited by period ('.') characters
val result: String = signedJWT.serialize
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.*;
import com.nimbusds.jwt.*;
// in case of a PEM key, first retrieve its JSON representation
// CAUTION: needs org.bouncycastle:bcpkix-jdk15on and org.bouncycastle:bcprov-jdk15on libraries
String jsonString = JWK.parseFromPEMEncodedObjects(pemString).toJSONString();
// we retrieve the RSA key from its JSON representation
RSAKey rsaJWK = RSAKey.parse(jsonString);
// Create RSA-signer with the private key
RSASSASigner signer = new RSASSASigner(rsaJWK);
// Prepare JWT with claims set
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder();
// add optional "id" claim
if (rsaJWK.getKeyID() != null) {
claimsSetBuilder.claim("id", rsaJWK.getKeyID());
}
// add "timestamp" claim
claimsSetBuilder.claim("timestamp", System.currentTimeMillis());
// prepare JWT with headers
JWSHeader.Builder headerBuilder = new JWSHeader.Builder(JWSAlgorithm.RS512);
// add optional key ID header
if (rsaJWK.getKeyID() != null) {
headerBuilder.keyID(rsaJWK.getKeyID());
}
// create signed JWT
SignedJWT signedJWT = new SignedJWT(headerBuilder.build(), claimsSet.build());
// Compute the RSA signature
signedJWT.sign(signer);
// Serialize to compact format consisting of Base64URL-encoded parts delimited by period ('.') characters
String result = signedJWT.serialize();
3. Sign in with server authentication method
Use the following snippet to sign-in the Webcom back end using the "server" authentication method (replace “<your-app>” with your actual application identifier):
// const app = Webcom.App("<your-app>"); // UNCOMMENT if you haven't yet an instance of your app!
// Get an instance of the authentication service
const auth = app.authentication;
// Sign in
const privateKey = {key: "-----BEGIN PUBLIC KEY-----\nqsdqsfjghgkjdgkjsgdk\n...\n-----END PUBLIC KEY-----"};
auth.signInWithServerKey("<yourServerKeyName>", privateKey)
.then(authDetails => console.log("Authentication succeeded", authDetails))
.catch(error => {
switch (error.code) {
case "INVALID_TOKEN":
console.log("The content or signature of the provided token is invalid.");
break;
case "PROVIDER_DISABLED":
console.log("The server keys based authentication method is disabled in the application. It must be enabled in the Webcom developer console.");
break;
default:
console.log("An unexpected error occurs, please retry and contact your administrator.", error);
}
})
The first parameter passed to signInWithServerKey
is the name of the key, as it was configured in the Webcom developer console.
The second parameter represent the key itself. It can be either a string with the name of a text file containing this
key (in the PEM format), or an object with a key
property providing this key (as illustrated above).
Use one of the following REST requests to authenticate on the Webcom back end using the "server" authentication method (replace “<your-app>” with your actual application identifier):
GET https://io.datasync.orange.com/auth/v2/<your-app>/server/signin?token=<the-JWT-token>
or (better)
POST https://io.datasync.orange.com/auth/v2/<your-app>/server/signin
// with the following body:
token:"<the-JWT-token>"
Then collect the resulting Webcom authentication token, which should look like (see Authentication state for details):
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDExMDgwND (...) NTliZjE1ZDRhIn19.kmiIQzimWXJUfdRyU6uf24gZMZ73TOZ2iE4SvAYsEhc",
"user": {
"displayName": "my key description",
"expires": 1601108041,
"provider": "server",
"context": [],
"createdAt": 1601021641959,
"providerUid": "myKeyName",
"uid": "43638923-1a67-49f5-a292-fce59bf15d4a"
}
}