Tutorial: Server login

Serverless AuthenticationSigning in and out Server login

Server login makes it possible to authenticate machines instead of real users, it is mainly aimed at opertations on the back end side. It is available on server- and IoT-oriented Webcom SDK (namely Java/Scala and soon 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.

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 base 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 ").

Code example to generate RSA keys

Here is an example of how to generate JSON Web RSA keys (JWK) with the Nimbus JOSE + JWT library:

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();

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 to null 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:

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();

Performing authentication

In order to authenticate on the Webcom back end using the "server" authentication method, you eventually have to use one of the following REST requests (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"
  }
}