User Tools

Site Tools


authentication:send_sms

This is an old revision of the document!


Overview

A system where users can log in using their phone number + SMS OTP instead of username/password, powered by Keycloak + Exotel.

Architecture

User (Browser/App)
↓ enter phone number
Node.js Server (64.227.190.56:3000)
↓ phone → find username by Keycloak admin API
↓ Keycloak login form proxy
Keycloak SPI (Java)
↓ OTP generate + store
↓ call webhook
Node.js Webhook
↓ call Exotel API
Exotel → SMS → User phone
↓ enter User OTP
Keycloak verify OTP→ return JWT Token</font>

Step 1: Created Custom Java SPI (Keycloak Plugin)

Step 1: Connect to DigitalOcean Server

1. Create Java SPI Project

Create folder structure

mkdir -p /root/sms-otp-spi/src/main/java/com/cotrav/keycloak\
mkdir -p /root/sms-otp-spi/src/main/resources/META-INF/services
==== File 1:   pom.xml   ====

<code>
''cat> /root/sms-otp-spi/pom.xml <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.cotrav.keycloak</groupId>
    <artifactId>keycloak-sms-otp</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <keycloak.version>26.1.0</keycloak.version>
    </properties>
    <dependencies>
        <dependency><groupId>org.keycloak</groupId><artifactId>keycloak-server-spi</artifactId><version>${keycloak.version}</version><scope>provided</scope></dependency>
        <dependency><groupId>org.keycloak</groupId><artifactId>keycloak-server-spi-private</artifactId><version>${keycloak.version}</version><scope>provided</scope></dependency>
        <dependency><groupId>org.keycloak</groupId><artifactId>keycloak-services</artifactId><version>${keycloak.version}</version><scope>provided</scope></dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive><manifest><addClasspath>false</addClasspath></manifest></archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
EOF

File 2: SmsOtpAuthenticator.java

cat> /root/sms-otp-spi/src/main/java/com/cotrav/keycloak/SmsOtpAuthenticatorFactory.java <<'EOF'\
package com.cotrav.keycloak;\
import org.keycloak.Config;\
import org.keycloak.authentication.Authenticator;\
import org.keycloak.authentication.AuthenticatorFactory;\
import org.keycloak.models.AuthenticationExecutionModel;\
import org.keycloak.models.KeycloakSession;\
import org.keycloak.models.KeycloakSessionFactory;\
import org.keycloak.provider.ProviderConfigProperty;\
import org.keycloak.provider.ProviderConfigurationBuilder;\
import java.util.List;

public class SmsOtpAuthenticatorFactory implements AuthenticatorFactory {\
  public static final String PROVIDER_ID = "sms-otp-authenticator";\
  private static final SmsOtpAuthenticator SINGLETON = new SmsOtpAuthenticator();

    @Override public String getId()          { return PROVIDER_ID; }\
  @Override public String getDisplayType() { return "SMS OTP Form"; }\
  @Override public String getHelpText()    { return "Sends OTP via SMS webhook and validates it."; }\
  @Override public String getReferenceCategory() { return "otp"; }\
  @Override public boolean isConfigurable()      { return true; }\
  @Override public boolean isUserSetupAllowed()  { return false; }

    @Override\
  public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {\
      return new AuthenticationExecutionModel.Requirement[]{\
          AuthenticationExecutionModel.Requirement.REQUIRED,\
          AuthenticationExecutionModel.Requirement.ALTERNATIVE,\
          AuthenticationExecutionModel.Requirement.DISABLED\
      };\
  }

    @Override\
  public List<ProviderConfigProperty> getConfigProperties() {\
      return ProviderConfigurationBuilder.create()\
          .property()\
              .name("smsWebhookUrl")\
              .label("SMS Webhook URL")\
              .helpText("URL of the webhook that sends SMS via Exotel")\
              .type(ProviderConfigProperty.STRING_TYPE)\
              .defaultValue("http://localhost:3000/webhook/send-sms")\
              .add()\
          .build();\
  }

    @Override public Authenticator create(KeycloakSession session) { return SINGLETON; }\
  @Override public void init(Config.Scope config) {}\
  @Override public void postInit(KeycloakSessionFactory factory) {}\
  @Override public void close() {}\
}\
EOF\

File 4: Service registration file

cat> /root/sms-otp-spi/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory <<'EOF'\
com.cotrav.keycloak.SmsOtpAuthenticatorFactory\
EOF

Step 4: Build JAR

''cd /root/sms-otp-spi
docker run --rm -v "$PWD":/app -w /app maven:3.9-eclipse-temurin-17 mvn clean package -q

Step 5: Deploy JAR to Keycloak

''docker cp /root/sms-otp-spi/target/keycloak-sms-otp-1.0.0.jar keycloak_app:/opt/keycloak/providers/
docker restart keycloak_app

# Verify JAR is loaded
docker exec keycloak_app ls /opt/keycloak/providers/

Useful Commands

# Check Node.js is running
ps aux | grep node

# View server logs
tail -f /root/keycloak-test/app.log

# Restart Node.js
pkill -f "node server.js"
cd /root/keycloak-test && nohup node server.js>> app.log 2>&1 &

# Restart Keycloak
docker restart keycloak_app

# Check Keycloak logs for errors
docker logs keycloak_app --tail=50 2>&1 | grep -i "error\|sms"

# Reload Nginx
nginx -s reload

Configured Keycloak Admin Console

1 — Created Authentication Flow

  • Admin Console → cotrav-OPS realm → Authentication → Create Flow
  • Flow name: Browser SMS OTP
  • Added steps:
    • Username Form → REQUIRED
    • SMS OTP Form (custom SPI) → REQUIRED

2 — Set Webhook URL in Flow

3 — Created sms-login-client

  • Admin Console → Clients → Create
  • Client ID = sms-login-client
  • Authentication Flow Override → Browser = Browser SMS OTP
  • Valid Redirect URIs = *

4 — Added phone to User Profile ⬅️ (This step was done manually)

  • Admin Console → cotrav-OPS realm → Realm Settings
  • Click User Profile tab
  • Click Add attribute
  • Attribute name: phone
  • Save

5 — Set Phone Number on User ⬅️ (Done manually)

  • Admin Console → Users → sonali.magar
  • Click Attributes tab
  • Add: phone = 8999463315
  • Save

6 — Set Login Theme

  • Realm Settings → Themes → Login Theme = cotrav
  • Required so Keycloak finds sms-otp-form.ftl template

Steps on digital Ocean

1. Install Node.js

curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
dnf install -y nodejs
node -v   # verify installation

2 : Create Node.js Project

mkdir -p /root/keycloak-test
cd /root/keycloak-test
npm init -y
npm install express cors axios

3. Start Server

nohup node server.js>> app.log 2>&1 &

4. Configured Nginx as Reverse Proxy

DigitalOcean firewall blocked port 3000. Added Node.js routes to existing Nginx config at /etc/nginx/conf.d/keycloak.conf:

location /api/ {
    proxy_pass http://localhost:3000;
    proxy_set_header X-Forwarded-Proto $scheme;
}
location /webhook/ {
    proxy_pass http://localhost:3000;
    proxy_set_header X-Forwarded-Proto $scheme;
}
nginx -s reload

Upload server.js from local machine (run on Windows):

scp d:/keycloak-test/server.js root@64.227.190.56:/root/keycloak-test/server.js

Start Node.js server:

cd /root/keycloak-test
nohup node server.js>> app.log 2>&1 &
# Check it started
tail -5 app.log

Step 7: Update Nginx Config

cat> /etc/nginx/conf.d/keycloak.conf <<'EOF'
server {
  listen 443 ssl;
  server_name 64.227.190.56;
    ssl_certificate     /etc/nginx/ssl/keycloak.crt;
  ssl_certificate_key /etc/nginx/ssl/keycloak.key;

    add_header Strict-Transport-Security "max-age=31536000" always;
  add_header X-Frame-Options SAMEORIGIN;
  add_header X-Content-Type-Options nosniff;

    # Node.js API
  location /api/ {
   proxy_pass http://localhost:3000;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-Proto $scheme;
  }

    # Webhook
  location /webhook/ {
      proxy_pass http://localhost:3000;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-Proto $scheme;
  }

    # Keycloak\
  location / {
      proxy_pass http://localhost:8080;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_buffer_size 128k;
      proxy_buffers 4 256k;
      proxy_busy_buffers_size 256k;
  }
}

server {
  listen 80;
  server_name 64.227.190.56;
  return 301 https://$host$request_uri;
}
EOF

# Test and reload Nginx

nginx -t && nginx -s reload
authentication/send_sms.1773039681.txt.gz · Last modified: by sonali