How do I use Google IAP for authenticating to JupyterHub? - jwt

I have Google's Identity-Aware Proxy configured for my JupyterHub application, and would like to use it to authenticate my users. How do I accomplish this?

Google passes signed JWT headers to applications with IAP in front of them, as described in Securing your app with signed headers. You can use mogthesprog/jwtauthenticator to enable JWT-based authentication in JupyterHub.
Configuring through jupyterhub_config.py
Once jupyterhub-jwtauthenticator is installed on your JupyterHub server, add the following configuration:
jupyterhub_config.py
c.JupyterHub.authenticator_class = 'jwtauthenticator.jwtauthenticator.JSONWebTokenAuthenticator'
c.JSONWebTokenAuthenticator.header_name = 'x-goog-iap-jwt-assertion'
c.JSONWebTokenAuthenticator.header_is_authorization = False
c.JSONWebTokenAuthenticator.expected_audience = '/projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID`
c.JSONWebTokenAuthenticator.username_claim_field = 'email'
# Retrieved from https://www.gstatic.com/iap/verify/public_key
c.JSONWebTokenAuthenticator.secret = """
{
"2nMJtw" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9e1x7YRZg53A5zIJ0p2ZQ9yTrgPL\nGIf4ntOk+4O2R2+ryIObueyenPXE92tYG1NlKjDNyJLc7tsxi0UUnyxpig==\n-----END PUBLIC KEY-----\n",
"6BEeoA" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElmi1hJdqtbvdX1INOf5B9dWvkydY\noowHUXiw8ELWzk/YHESNr8vXQoyOuLOEtLZeCQbFkeLUqxYp1sTArKNu/A==\n-----END PUBLIC KEY-----\n",
"FAWt5w" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8auUAdTS54HmUuIabrTKvWawxmbs\n81kdbzQMV/Tae0EhLgin8qnJ4lklJrxEzksXg5OtBuzE62DIj+CePN20Pg==\n-----END PUBLIC KEY-----\n",
"LYyP2g" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESlXFFkJ3JxMsXyXNrqzE3ozl/091\n3PmNbccLLWfeQFUYtJqGtl8ESuYxRwc/QwZp5Wcl0HCq6GuFDx4/Tk18Ig==\n-----END PUBLIC KEY-----\n",
"f9R3yg" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESqCmEwytkqG6tL6a2GTQGmSNI4jH\nYo5MeDUs7DpETVhCXXLIFrLg2sZvNqw8SGnnonLoeqgOSqRdjJBGt4I6jQ==\n-----END PUBLIC KEY-----\n"
}
"""
Configuring through zero-to-jupyterhub-k8s
If you're using zero-to-jupyterhub-k8s to provision JupyterHub in a Kubernetes cluster, you will first need to bake the jupyterhub-jwtauthenticator package into your hub image.
With the following Dockerfile and cloudbuild.yaml in a directory, execute gcloud builds submit --config cloudbuild.yaml . using the appropriate project.
Dockerfile
FROM jupyterhub/k8s-hub:0.7.0
RUN pip3 install --no-cache-dir jupyterhub-jwtauthenticator
cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
args: [ 'pull', 'docker.io/jupyterhub/k8s-hub:0.7.0' ]
- name: 'gcr.io/cloud-builders/docker'
args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/k8s-hub', '.' ]
images:
- 'gcr.io/$PROJECT_ID/k8s-hub'
values.yaml
hub:
image:
name: gcr.io/<project>/k8s-hub
tag: latest
auth:
type: custom
custom:
className: 'jwtauthenticator.jwtauthenticator.JSONWebTokenAuthenticator'
config:
header_name: x-goog-iap-jwt-assertion
header_is_authorization: false
expected_audience: '/projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID'
username_claim_field: email
# Retrieved from https://www.gstatic.com/iap/verify/public_key
secret: |
{
"2nMJtw" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9e1x7YRZg53A5zIJ0p2ZQ9yTrgPL\nGIf4ntOk+4O2R2+ryIObueyenPXE92tYG1NlKjDNyJLc7tsxi0UUnyxpig==\n-----END PUBLIC KEY-----\n",
"6BEeoA" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElmi1hJdqtbvdX1INOf5B9dWvkydY\noowHUXiw8ELWzk/YHESNr8vXQoyOuLOEtLZeCQbFkeLUqxYp1sTArKNu/A==\n-----END PUBLIC KEY-----\n",
"FAWt5w" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8auUAdTS54HmUuIabrTKvWawxmbs\n81kdbzQMV/Tae0EhLgin8qnJ4lklJrxEzksXg5OtBuzE62DIj+CePN20Pg==\n-----END PUBLIC KEY-----\n",
"LYyP2g" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESlXFFkJ3JxMsXyXNrqzE3ozl/091\n3PmNbccLLWfeQFUYtJqGtl8ESuYxRwc/QwZp5Wcl0HCq6GuFDx4/Tk18Ig==\n-----END PUBLIC KEY-----\n",
"f9R3yg" : "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESqCmEwytkqG6tL6a2GTQGmSNI4jH\nYo5MeDUs7DpETVhCXXLIFrLg2sZvNqw8SGnnonLoeqgOSqRdjJBGt4I6jQ==\n-----END PUBLIC KEY-----\n"
}
Retrieving IAP public keys automatically
If you don't want to bake the public keys for IAP JWT tokens into your configuration, you can retrieve these at run-time through your jupyterhub_config.py, or as extraConfig in the helm chart.
jupyterhub_config.py
from urllib import request
c.JSONWebTokenLocalAuthenticator.secret = request.urlopen('https://www.gstatic.com/iap/verify/public_key').read()
For zero-to-jupyterhub-k8s:
values.yaml
hub:
extraConfig:
config-jwtauthenticator: |
from urllib import request
c.JSONWebTokenLocalAuthenticator.secret = request.urlopen('https://www.gstatic.com/iap/verify/public_key').read()

Related

Sealed secrets with YAML list

I got a list in yml - credentials. And supposedly each bank has to have a different password that needs to be encrypted. What would be the right way to specify that? As of now I got it configured like this, but that doesn't work.
This is the config.yml
infopoint:
endpoint: https://test.test.com/ws/SSS/Somthing.pl
system: TEST
mock: false
credentials:
- bank: 1111
user: LSSER
existingSecret:
name: infopoint-creds-s1-hb
- bank: 2222
user: TESSER
existingSecret:
name: infopoint-creds-s1
envFrom:
- secretRef:
name: infopoint-creds-s1-hb
- secretRef:
name: infopoint-creds-s1
This is how I created both secret keys on the server.
C:\Users\mks\IdeaProjects>kubectl.exe create secret generic infopoint-creds-s1-hb --from-literal=INFOPOINT_CREDENTIALS_PASSWORD=SOMEPASS -o yaml -n test-env --dry-run=client | kubeseal -o yaml --scope namespace-wide > infopoint-creds-s1-hb.yaml
C:\Users\mks\IdeaProjects>kubectl.exe create secret generic infopoint-creds-s1 --from-literal=INFOPOINT_CREDENTIALS_PASSWORD=SOMEPASS -o yaml -n test-env --dry-run=client | kubeseal -o yaml --scope namespace-wide > infopoint-creds-s1.yaml
This is my Spring configuration.
#Configuration
#ConfigurationProperties(prefix = "infopoint")
class InfopointAPIConfiguration {
lateinit var endpoint: String
var proxyServerName: String? = null
var proxyPortNumber: String? = null
lateinit var system: String
lateinit var mock: String
lateinit var credentials: List<Credentials>
data class Credentials(
var bank: String? = null,
var user: String? = null,
var password: String? = null
)
fun credentialsByBank(bank: Int): Credentials {
return credentials.firstOrNull { it.bank == bank.toString() }
?: error("Could not load credential for bank $bank")
}
}
Kubernetes secrets can be used or configured in applications in multiple ways for example configmaps, sealed secrets and environment variables. Since you got struck with the sealed secrets part I am providing an answer related to the same.
First we need to create a sealed secret in the same namespace with the same name for preventing other users on the same cluster from using your sealed secret. For more information related to sealed secrets please go through this document.
Now we have our secret created, all we need to do is to use it in our application. The secret which we created needs to be referenced in the yaml file. There is a detailed description on how to configure secrets in a spring boot application along with a sample project available here.

illegal base64 data at input byte 4852 with sealed secrets

I have this secret built like this:
apiVersion: v1
kind: Secret
metadata:
name: secrets
type: Opaque
stringData:
PORT: "3000"
MONGODB_HOSTNAME: hostname
MONGODB_USER: fares
MONGODB_PASSWORD: password
MONGODB_TEST_HOSTNAME: localhost
ENCODED_CREDENTIALS: ewog...
ENCODED_CREDENTIALS contains an encoded base64 JSON file. It's decoded in the app but I need it to remain encoded as a secret.
Now, I seal it using my cert and kubeseal, but ArgoCD displays this: ErrUnsealFailed - Failed to unseal: illegal base64 data at input byte 4852.
I feel like it's the fact that ENCODED_CREDENTIALS is already encoded that might be a problem, but I'm not sure.
Are you guys familiar with this kind of problems?
TIA
EDIT:
The decoding (pre-seal) works:
echo -n $test_thing | base64 -d
{
"type": "service_account",
"project_id": "blabla",
"private_key_id": "blabla",
"private_key": "-----BEGIN PRIVATE KEY-----
...
\n-----END PRIVATE KEY-----\n",
"client_email": "john.doe#gserviceaccount.com",
"client_id": "abcd1234",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/bucket-sa%40steve-jobs.iam.gserviceaccount.com"
}
It's only when it seals & unseals with the sealed secrets controller that it fails.
Good news!
I've managed to find where the issue is.
For some reason, adding an (intentionally) encoded value to stringData keys breaks the unsealing process.
For those who are curious, I've raised the bug (?) to the sealed secrets team: #1038

Generate EdDSA 25519 key pair for JOSE/NODEJS

Here is the command I used on ubuntu 20.x to generate key pair of EdDSA 25519 for JOSE/NODEJS (14.16) app:
$ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id-ed25519 -C myemail_address
Here is the private key generated:
-----BEGIN OPENSSH PRIVATE KEY-----
a3BlbnNzaC1rZXktdjEAAAAABG5vbmVAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAp92w+fwodL4kaUDrNghdZScdcg54IJOO6tLpG91oeKgAAAJj71Y9w+9WP
cAAAAAtzc2gtZWQyNTUxOQAAACAp92w+awodL4kaUDrNghdZScdcg54IJOO6tLpG91oeKg
AAAEDsEfbdyx4HaM5cL1f2Ag2Knb0NDCIiuIDsm6FwR5NJESn3bD5/Ch0viRpQOs2CF1lJ
c1yDnggk47q0ukb3Wh4qAAAAFGVtY2yhYjIwMTFAZ21haWwuY29tAQ==
-----END OPENSSH PRIVATE KEY-----
The private key has 366 bytes instead of 32 bytes.
Here is the public key:
BAAAC3NzaC1lZ1I1NTE5AAAAICn3CD5/Ch0viRpQOs2CF1lJx1yDnggk47q0ukb3Wh4q myemail_address
It is 63bytes without counting email address and seems too long.
Is it the right way to generate key pair for EdDSA 25519? If it is not, what is the right way?
You can use Node.js (>= 12.0.0) for this as well.
const keypair = crypto.generateKeyPairSync(
'ed25519',
{
privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
publicKeyEncoding: { format: 'pem', type: 'spki' }
}
)
console.log(keypair.privateKey)
console.log(keypair.publicKey)
There is both blocking and non-blocking API for this.

Exceptions connecting to MongoDB Replica Set hosted inside Kubernetes with bitnami Helm chart

We experienced some problems (timeout and notPrimary Exceptions) if we tried connecting to a MongoDB Replica Set hosted inside a kubernetes environment. We used bitnami helm chart 8.2.1.
If we connect to an on-premise Replica Set installation of MongoDB 4.2.3, everything is ok. So we assume the problem must be with the configuration of the kubernetes installation. If I try to connect with MongoDB Compass to the kubernetes hosted instance, it worked but there are no other nodes visible in the HOSTS area. Connecting to the on-prem installation, there are all nodes visible.
Using the on-prem installation is not an option, because we want that every of our developers has its own database (and of course the same configuration!).
We installed the Replica set with the following script:
helm repo add bitnami https://charts.bitnami.com/bitnami
# changing architecture or replica count, you must delete the persistance volume claims, otherwise mongodb starts with the previous configuration.
$values = #"
architecture: replicaset
replicaCount: 2
replicaSetName: testRS
auth:
enabled: true
rootPassword: admin
"#;
<# if I add these part to the above values variable, no connection could be established. Neither from MongoDB Compass nor from C# client
externalAccess:
enabled: true
service:
type: LoadBalancer
port: 27017
autoDiscovery:
enabled: true
serviceAccount:
create: true
rbac:
create: true
#>
if ( (helm list --namespace="infrastructure" --filter="mongodb" -q) -eq "mongodb") {
$values | helm upgrade mongodb bitnami/mongodb --values - --namespace infrastructure --recreate-pods --version 8.2.1
}
else {
$values | helm install mongodb bitnami/mongodb --values - --namespace infrastructure --version 8.2.1
}
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
$values = #"
tcp:
# MongoDb
27017: "infrastructure/mongodb-headless:27017"
"#;
if ( (helm list --namespace="default" --filter="ingress-nginx" -q) -eq "ingress-nginx") {
$values | helm upgrade ingress-nginx ingress-nginx/ingress-nginx --values - --namespace default
}
else {
$values | helm install ingress-nginx ingress-nginx/ingress-nginx --values - --namespace default
}
The dotnet core sample application:
using System;
using System.Threading.Tasks;
using MongoDB.Driver;
namespace MongoDBReplicaSet {
class Program {
static async Task Main(string[] args) {
// Connecting to an on-premise installation (1 primary, 2 secondary) worked without any problems.
//var connectionString = "mongodb://root:admin#on-prem:27080/?authSource=admin&replicaSet=testRS&readPreference=primary";
var connectionString =
"mongodb://root:admin#localhost:27017/?authSource=admin&readPreference=primary";
await SimpleTask(connectionString);
await Transaction(connectionString);
Console.WriteLine("done.");
}
static async Task SimpleTask(string connectionString) {
var client = new MongoClient(connectionString);
var database = client.GetDatabase("TestDB");
var collection = database.GetCollection<TestData>(nameof(TestData));
/*
* Can not connect to the kubernetes hosted replica set. MongoDB Compass and CLI can connect with the same connection string,
* but in Compass, you don't see the other nodes under the HOSTS label
*
* System.TimeoutException: 'A timeout occured after 30000ms selecting a server using CompositeServerSelector{
* Selectors = WritableServerSelector, LatencyLimitingServerSelector{ AllowedLatencyRange = 00:00:00.0150000 } }.
* Client view of cluster state is { ClusterId : "1", ConnectionMode : "Automatic", Type : "ReplicaSet",
* State : "Connected", Servers : [{ ServerId: "{ ClusterId : 1, EndPoint : "Unspecified/localhost:27017" }",
* EndPoint: "Unspecified/localhost:27017", ReasonChanged: "Heartbeat", State: "Connected", ServerVersion: 4.2.8,
* TopologyVersion: , Type: "ReplicaSetSecondary", WireVersionRange: "[0, 8]" ...
*
*
* if eliminating the replicaset parameter from the connection string, the exception is different:
* MongoDB.Driver.MongoNotPrimaryException: 'Server returned not master error.', but sometimes it worked.
* It seems the client connecting by random to the primary and the other time to the secondary?
*/
await collection.InsertOneAsync(new TestData(Guid.NewGuid().ToString(), "Test1", new DateTimeOffset(DateTime.Now)));
Console.WriteLine("done simple task.");
}
static async Task Transaction(string connectionString) {
var client = new MongoClient(connectionString);
var database = client.GetDatabase("TestDB");
// the same exception.
var collection = database.GetCollection<TestData>(nameof(TestData));
using (var session = await client.StartSessionAsync()) {
session.StartTransaction();
try {
await collection.InsertOneAsync(new TestData(Guid.NewGuid().ToString(), "Test2", new DateTimeOffset(DateTime.Now)));
await session.CommitTransactionAsync();
}
catch (Exception ex) {
await session.AbortTransactionAsync();
}
}
Console.WriteLine("done transaction.");
}
}
public class TestData {
public string Id { get; set; }
public string Name { get; set; }
public DateTimeOffset CreatedAt { get; private set; }
public TestData(string id, string name, DateTimeOffset createdAt) {
Id = id;
Name = name;
CreatedAt = createdAt;
}
}
}
Has anyone a solution for using MongoDB Replica Set inside Kubernetes and accessing it without any exceptions from outside?
I would be very thankful for helping us.

Invalid signature while signing JWT with RSA Keys using https://jwt.io/

Hi I am testing some api through POSTMAN.
REGISTRATION API- It takes public key and stores in server.To get public JWK Key I am using https://mkjwk.org/. It generates a key with public and private key. Below is the key generated:
{
"kty": "RSA",
"d": "HgP6c8xA3D_-8DKgSk3fQ_FZuPj2RNSFE5NLfGz3GJjkyt9fzOPztNObQmLZ2EoJzPrYL8ljk-1mKGIr5Ma1n4TPX_kQ9JErq9wNyhMUTykQ8PqjMuxmUpddn43RZ27VPjvUvHMulk5hPBFv0uH3LnDsM1xn34icj40y4zcRTYXwixqDgj74Ua_9aaZwPXX06Ykc3vzC5M4F5JP4ZjuLTFjmM8jxzJpu4JNDncXwhHE5Xtv3t1oezXTHNZdRzyTTtFLOeyYeLt5WWM-y549du3hsUtvgNnJ4JTS4uEHEonDMW8CV5ZzoQE8F81LT5Q84coYzOjVlzC50w7st3vLFYQ",
"e": "AQAB",
"use": "sig",
"kid": "sdk291",
"alg": "RS256",
"n": "0h5Pgw32t3NtODVj1UrNkk90RytZBz2T09JBEo1NwYtw0T7NM3SrFsleLK8e_DknE26rB8EXD2T1YmUSuseuWvW-LPdwbk--SrKo8Df1H5ff5hDZYJ_x-NPDJ_ZmoY7r2U83aGY-DubilufhDf6icB2auTGxBvVylAl5Jdf7UBsjHAWrgWAtFFvnkbcaUk1O7ZA8nS4Iyk4l8vVsoUOJCSRrysceObAOG-EYj1UfdzOBZsuSj5Usst6ebRjF9M1VNJOLcvm6EiAqazayyqbsengnA-hqUGRgfd6HXXZ5Hg4BU_srPVBXrVmx81azHY5lRUdZr_Khtw4O0Zy3UIwMWw"
}
I am only sending public key portion to this API and this API Works fine.
GETTING ACCESS TOKEN USING JWT API- This takes a JWT which will be created with some claims . And we have to sign it with RSA Public and private key. I am facing problem in signing the JWT.
To get text/pem version of public and private keys from above JWK, I am using https://8gwifi.org/jwkconvertfunctions.jsp . This gives us below public and private key.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0h5Pgw32t3NtODVj1UrN
kk90RytZBz2T09JBEo1NwYtw0T7NM3SrFsleLK8e/DknE26rB8EXD2T1YmUSuseu
WvW+LPdwbk++SrKo8Df1H5ff5hDZYJ/x+NPDJ/ZmoY7r2U83aGY+DubilufhDf6i
cB2auTGxBvVylAl5Jdf7UBsjHAWrgWAtFFvnkbcaUk1O7ZA8nS4Iyk4l8vVsoUOJ
CSRrysceObAOG+EYj1UfdzOBZsuSj5Usst6ebRjF9M1VNJOLcvm6EiAqazayyqbs
engnA+hqUGRgfd6HXXZ5Hg4BU/srPVBXrVmx81azHY5lRUdZr/Khtw4O0Zy3UIwM
WwIDAQAB
-----END PUBLIC KEY-----
-----BEGIN RSA PRIVATE KEY-----
MIICHgIBAAKCAQEA0h5Pgw32t3NtODVj1UrNkk90RytZBz2T09JBEo1NwYtw0T7N
M3SrFsleLK8e/DknE26rB8EXD2T1YmUSuseuWvW+LPdwbk++SrKo8Df1H5ff5hDZ
YJ/x+NPDJ/ZmoY7r2U83aGY+DubilufhDf6icB2auTGxBvVylAl5Jdf7UBsjHAWr
gWAtFFvnkbcaUk1O7ZA8nS4Iyk4l8vVsoUOJCSRrysceObAOG+EYj1UfdzOBZsuS
j5Usst6ebRjF9M1VNJOLcvm6EiAqazayyqbsengnA+hqUGRgfd6HXXZ5Hg4BU/sr
PVBXrVmx81azHY5lRUdZr/Khtw4O0Zy3UIwMWwIBAAKCAQAeA/pzzEDcP/7wMqBK
Td9D8Vm4+PZE1IUTk0t8bPcYmOTK31/M4/O005tCYtnYSgnM+tgvyWOT7WYoYivk
xrWfhM9f+RD0kSur3A3KExRPKRDw+qMy7GZSl12fjdFnbtU+O9S8cy6WTmE8EW/S
4fcucOwzXGffiJyPjTLjNxFNhfCLGoOCPvhRr/1ppnA9dfTpiRze/MLkzgXkk/hm
O4tMWOYzyPHMmm7gk0OdxfCEcTle2/e3Wh7NdMc1l1HPJNO0Us57Jh4u3lZYz7Ln
j127eGxS2+A2cnglNLi4QcSicMxbwJXlnOhATwXzUtPlDzhyhjM6NWXMLnTDuy3e
8sVhAgEAAgEAAgEAAgEAAgEA
-----END RSA PRIVATE KEY-----
I am taking these keys and going to https://jwt.io/ , then updating Header and payload with my required values.
Header:
{
"alg": "RS256",
"typ": "JWT"
}
Payload :
{
"iss": "e9064e3cb1cc445cb2095f3dc675b4gf",
"sub": "e9064e3cb1cc445cb2095f3dc675b4gf",
"aud": [
"southgate"
],
"iat": 1543469348,
"nbf": 1543469348,
"exp": 1543517174,
"jti": "a6fb0873-8653-4923-be47-e53e0acabb35",
"schemas": [
"urn:x:y:v1"
],
"ten": "myTen"
}
After this when I sign the JWT including public and private key from above, It doesnt generate JWT token and gives "INVALID SIGNATURE" error. SO I am not able to generate the signed JWT which is needed as input for this 2nd API.
PS- I am in testing phase now. So I havent wrote any code to test the api and using only online generator tool.
Please let me know how I can solve the issue.