Central Authentication Service SDK
Introduction
This SDK is used as a client for interfacing with the Shib Central Authentication Service (CAS). This service is a decentralized authentication system that leverages Ethereum addresses and cryptographic signatures for secure user authentication. It ensures unique request validation and signature verification, making it suitable for applications requiring blockchain-based authentication.
The service supports replay attack prevention through unique request IDs and verifies user identities by recovering addresses from Ethereum signatures.
The Shib Central Authentication Service is responsible for generating secure, signed JSON Web Tokens (JWT) using the ES512 (ECDSA with SHA-512) algorithm with expiration.
The SDK also supports account abstraction.
Audience
Developers.
Installation
Prerequisites
- Your wallet address must have a valid DID registered to use CAS.
- Addresses with valid DIDs can use this SDK.
Install the SDK
Type the following command to install the SDK.
pnpm i @shibaone/cas-sdk
Local Development
Install Deps -> Build -> Publish Locally
Install Dependencies
pnpm install
Build
Build with package at /lib with vite:
pnpm build:vite
Publish Locally
If npm package not available. First install yalc, publish the package -
yalc push --sig
Use Vite in dependent projects for a smoother experience as Vite has native WASM support.
Testing
Set up .env file by referring .example.env:
cp .env.example .env
Run the tests:
pnpm test
To view coverage report, open coverage/lcov-report/index.html in any browser.
Check test/cas.test.ts on how to use the SDK with testnet.
Getting Started
- Import CAS-SDK
- Import signer from ShibAuthSDK/ethers.js
- Create an instance of CAS-SDK
- Call
authenticatemethod of CAS-SDK - The authenticate method returns a JWTToken.
import { ethers } from "ethers";
import { CASDK } from "CASDK";
const signer = new ethers.Wallet(privateKey, provider);
const caSDK = await CASDK.create({
signer,
});
const jwtToken = await caSDK.authenticate();
Types
Here are the types that appear in this SDK:
type SDKCreationParams = {
signer: Signer;
casURL?: string;
};
type DecodedToken = {
did: string;
exp: number;
};
type JWKS = {
keys: [
{
kty: string;
use: string;
key_ops: [string];
alg: string;
kid: string;
crv: string;
x: string;
y: string;
}
];
};
type AuthorizeParams = {
secretKey: string;
authenticateJwtToken: string;
casUrl?: string;
}
Constants
Constants used in this SDK:
const CAS_URL = "https://cas.shibinternal.com";
Utils
authorize
async function authorize (params: AuthorizeParams)
An utility function used to authorize the user for allowing the access with our SDKs services. It MUST be used on your Backend.
Example:
import { CAS_URL } from '@shibaone/cas-sdk/costants'
import { authorize } from '@shibaone/cas-sdk/utils'
const jwt = await autorize({
secretKey: "Your secret key retrieved by the Shiba Developer Portal"
authenticateJwtToken: "Your JWT token retrived from the method authenticate in the client side"
casUrl: CAS_URL
})
Always keep your secretKey secure and never expose it on the client side.
Integrate the authorization process in other languages
If you're not using Node.js as your backend, you can still integrate the authorization process in other languages. Below are some language-specific examples to help you implement it:
python
import requests
class AuthorizeParams:
def __init__(self, secret_key, authenticate_jwt_token, cas_url=None):
self.secret_key = secret_key
self.authenticate_jwt_token = authenticate_jwt_token
self.cas_url = cas_url
def authorize(params: AuthorizeParams) -> str:
try:
headers = {
"x-secret-key": params.secret_key,
"Authorization": f"Bearer {params.authenticate_jwt_token}",
}
url = f"{params.cas_url}/api/v1/authorize"
response = requests.post(url, headers=headers)
response.raise_for_status()
jwt_token = response.json().get("jwtToken")
return jwt_token
except Exception as e:
raise RuntimeError(f"authenticate: {str(e)}")
go
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
)
type AuthorizeParams struct {
SecretKey string
AuthenticateJwtToken string
CasUrl string
}
func Authorize(params AuthorizeParams) (string, error) {
url := fmt.Sprintf("%s/api/v1/authorize", params.CasUrl)
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte("{}")))
if err != nil {
return "", fmt.Errorf("authenticate: %v", err)
}
req.Header.Set("x-secret-key", params.SecretKey)
req.Header.Set("Authorization", "Bearer "+params.AuthenticateJwtToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("authenticate: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("authenticate: status %v", resp.Status)
}
var result map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return "", fmt.Errorf("authenticate: %v", err)
}
jwtToken, ok := result["jwtToken"].(string)
if !ok {
return "", errors.New("authenticate: invalid jwtToken")
}
return jwtToken, nil
}
Java
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.OutputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.util.Map;
import com.google.gson.Gson;
public class AuthorizeParams {
public String secretKey;
public String authenticateJwtToken;
public String casUrl;
public AuthorizeParams(String secretKey, String authenticateJwtToken, String casUrl) {
this.secretKey = secretKey;
this.authenticateJwtToken = authenticateJwtToken;
this.casUrl = casUrl;
}
}
public class Authorize {
public static String authorize(AuthorizeParams params) throws Exception {
String urlString = params.casUrl + "/api/v1/authorize";
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("x-secret-key", params.secretKey);
conn.setRequestProperty("Authorization", "Bearer " + params.authenticateJwtToken);
conn.setDoOutput(true);
// Send an empty body
try (OutputStream os = conn.getOutputStream()) {
os.write("{}".getBytes());
os.flush();
}
int responseCode = conn.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new Exception("authenticate: HTTP error code " + responseCode);
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
Gson gson = new Gson();
Map<String, Object> response = gson.fromJson(br, Map.class);
return (String) response.get("jwtToken");
}
}
}
Methods
Constructor
constructor(signer: Signer, casURL: string)
Description
- Initialises a new instance of the CASDK class with a provided Signer object.
- A Signer is an abstraction of an Ethereum Account (Externally Owned Account-EOA), which can be used to sign messages and transactions and send signed transactions to the Blockchain Network to execute state changing operations.
casURLthe endpoint of Shib Central Authentication Service.
Returns
- The above method returns an instance of the CASDK.
Example:
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider((window as any).ethereum);
const signer = await provider.getSigner();
const caSDK = new CASDK(signer);
create
static async create(params: SDKCreationParams): Promise <CASDK>
Description
- A factory method that creates an instance of the CASDK class. It accepts parameters as defined by SDKCreationParams, including the Signer object, and returns a new instance of the CASDK class.
Parameters
| parameter | required/optional | type | description |
|---|---|---|---|
| SDKCreationParams | required | Parameters as defined by SDKCreationParams, including the Signer object. | |
| signer | required | Signer | An abstraction of an Ethereum Account (Externally Owned Account-EOA), which can be used to sign messages and transactions. |
| casURL | optional | string | The endpoint of Shib Central Authentication Service. |
Example:
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider((window as any).ethereum);
const signer = await provider.getSigner();
const caSDK = await CASDK.create({
signer,
});
Returns
- The above method returns an instance of the CASDK.
getRequestId
private async getRequestId(signer: Signer, chainId: string, AAaddress?: string): Promise<string>
Description
- A private method that retrieves a request ID from CAS's API for a given Signer and chainId. This is used in the authentication process.
- This method is not callable from the class instance, hence, no example provided for it.
Parameters
| parameter | required/optional | type | description |
|---|---|---|---|
| signer | required | Signer | An abstraction of an Ethereum Account (Externally Owned Account-EOA), which can be used to sign messages and transactions. |
| chainId | required | string | Blockchain ID. |
| AAaddress | optional | string | Account Abstraction address. If not provided, the address is retrieved from signer, if provided AAadress is used to get the request ID. |
Returns: Promise<string>
authenticate
public async authenticate(AAaddress?: string): Promise<string>
Description
- Authenticates the user by signing a message with their Signer, sending it to CAS's API, validating the response, and returning a JWT token if successful.
Parameters
| parameter | required/optional | type | description |
|---|---|---|---|
| AAaddress | optional | string | Account Abstraction address. If not provided, the address is retrieved from signer, if provided AAadress is used to get the request ID. |
Returns: Promise<string>
- The above method returns a JWT token, if the signer is DID registered else it returns 401
Example:
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider((window as any).ethereum);
const signer = await provider.getSigner();
const caSDK = await CASDK.create({
signer,
});
// It returns a JWT token, given the signer is DID registered
const jwtToken = await caSDK.authenticate();
decode
public async decode(token: string): Promise<DecodedToken>
Description
- Decodes a provided JSON Web Token using the jsonwebtoken library's decode method.
Parameters
| parameter | required/optional | type | description |
|---|---|---|---|
| token | required | string | JSON Web Token. |
Returns
- The above method returns the decoded token given a JWT
Example:
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider((window as any).ethereum);
const signer = await provider.getSigner();
const caSDK = await CASDK.create({
signer,
});
// It returns a JWT token, given the signer is DID registered
const jwtToken = await caSDK.authenticate();
const decodedToken = await caSDK.decode(jwtToken);
console.log(`Decoded token is: ${JSON.stringify(decodedToken)}`)
verify
public async verify(token: string): Promise<DecodedToken>
Description
- Verifies the validity of a provided JSON Web Token by fetching a JWKS endpoint from CAS, extracting the signing key, and verifying the token with this key.
Parameters
| parameter | required/optional | type | description |
|---|---|---|---|
| token | required | string | JSON Web Token. |
Returns
- It returns the DecodedToken object.
- The above method returns the verified token if token is valid else it returns status 500
Example:
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider((window as any).ethereum);
const signer = await provider.getSigner();
const caSDK = await CASDK.create({
signer,
});
// It returns a JWT token, given the signer is DID registered
const jwtToken = await caSDK.authenticate();
const verifiedToken = await caSDK.verify(jwtToken);
console.log(`Verified token is: ${JSON.stringify(verifiedToken)}`)
jwks
public async jwks(): Promise<JWKS>
Description
- Returns the token signing keys which can be used for verifying the authenticity of the tokens.
Returns: Promise<JWKS>
Example:
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider((window as any).ethereum);
const signer = await provider.getSigner();
const caSDK = await CASDK.create({
signer,
});
const jwksKeys = await caSDK.jwks();
Error Handling
- Authentication
CASDK.authenticate()fails when signer does not have a valid DID- Error response:
authenticate: Error: getRequestId: Signer does not have a valid DID
- Error response:
Configuration Options
- Passing
Signerobject is mandatory for creating an instance of the SDK.