A Telegram bot for auth and login to a web domain.
This provides a relatively easy way to provide authentication and authorisation for any domain:
- no signup required, just use Telegram
- no email verification required, as you have the authentic Telegram username
- works great on mobile, use the Telegram app
- works great on desktop, use https://web.telegram.org
- minimal code required, just verify the session cookie via Redis
Deploy a bot to authenticate and authorise users for your own domains:
- create an auth bot for your domain e.g.
@adhocDomainAuthBot
via command/newbot
to the Telegram@BotFather
- configure and deploy this auth bot service on your domain for location
/authbot/
e.g. using a Docker image - set the bot webhook via
api.telegram.org
to/authbot/webhook/
- as a user, send the command
/login
- voila!
How it works:
- your authbot will reply to the user with a magic pending login link to itself e.g.
/authbot/login/${user}/${token}
- the user clicks on that
/authbot/login/
link in the chat with the authbot - the authbot HTTP handler for
/authbot/login/
will create the session in Redis, set the session cookie on the HTTP response, and redirect to your landing page for auth'ed visitors e.g./auth
- your site can verify the session cookie via Redis or HTTPS
- the original authoritative (admin) Telegram username can authorise other users
We use Koa:
async function startHttpServer() {
api.post('/authbot/webhook/:secret', async ctx => {
ctx.body = '';
if (ctx.params.secret !== config.secret) {
logger.debug('invalid', ctx.request.url);
} else {
await handleMessage(ctx.request.body);
}
});
api.get('/authbot/login/:username/:token', async ctx => {
await handleIn(ctx);
});
api.get('/authbot/logout', async ctx => {
await handleOut(ctx);
});
Additionally the following endpoint can allow session validation via HTTP:
if (config.sessionRoute) {
api.get('/authbot-session/:username/:sessionId', async ctx => {
await handleSession(ctx);
});
}
where this location is deliberately different from /authbot/
so that it must be specifically allowed e.g. by your Nginx API gateway.
The /authbot/login/
HTTP handler will set the session cookie:
assert.equal(login.username, username, 'username');
assert.equal(login.token, token, 'token');
const sessionId = [token, generateToken(16)].join('_');
const sessionKey = [config.namespace, 'session', sessionId, 'h'].join(':');
const sessionListKey = [config.namespace, 'session', username, 'l'].join(':');
const session = Object.assign({}, login, {started: Date.now()});
const [hmset] = await multiExecAsync(client, multi => {
multi.hmset(sessionKey, session);
multi.expire(sessionKey, config.sessionExpire);
multi.del(loginKey);
multi.lpush(sessionListKey, sessionId);
multi.ltrim(sessionListKey, 0, 3);
});
ctx.cookies.set('sessionId', sessionId, {maxAge: config.cookieExpire, domain: config.domain, path: '/'});
ctx.redirect(config.redirectAuth);
For demo purposes we also serve the following landing pages, which would ordinarily be served by the app:
if (config.demo) {
api.get('/auth', async ctx => {
await handleAuth(ctx);
});
api.get('/noauth', async ctx => {
await handleNoAuth(ctx);
});
api.get('/', async ctx => {
await handleHome(ctx);
});
}
where /auth
and /noauth
are redirects from /authbot/login
The login is created in Redis by the Telegram bot, which provides the /authbot/login/
"magic link."
async function handleTelegramLogin(request) {
const {username, name, chatId} = request;
const token = generateToken(16);
const loginKey = [config.namespace, 'login', username, 'h'].join(':');
let [hmset] = await multiExecAsync(client, multi => {
multi.hmset(loginKey, {token, username, name, chatId});
multi.expire(loginKey, config.loginExpire);
});
if (hmset) {
await sendTelegramReply(request, 'html', [
`You can login via https://${[config.domain, 'authbot', 'login', username, token].join('/')}.`,
`This link expires in ${config.loginExpire} seconds.`,
`Powered by https://github.com/evanx/authbot.`
where a secret token is randomly generated for the "magic link."
function generateToken(length = 16) {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const charset = '0123456789' + letters + letters.toLowerCase();
return crypto.randomBytes(length).map(
value => charset.charCodeAt(Math.floor(value * charset.length / 256))
).toString();
}
The default configuration properties are hard-coded as follows:
const configDefault = {
port: 8080,
namespace: 'authbot',
redisHost: '127.0.0.1',
loginExpire: 30,
sessionExpire: 300,
cookieExpire: 60000,
sendTimeout: 8000,
redirectAuth: '/auth',
redirectNoAuth: '/noauth',
sessionRoute: true,
loggerLevel: 'debug'
};
where namespace
is used to prefix auth bot keys in Redis, for pending logins and active sessions.
The following declares meta information about further required configuration:
const configMeta = {
domain: {
description: 'HTTPS web domain to auth access',
example: 'authdemo.webserva.com'
},
bot: {
description: 'Telegram Bot name i.e. this authbot',
example: 'ExAuthDemoBot',
info: 'https://core.telegram.org/bots/api',
hint: 'https://telegram.me/BotFather'
},
secret: {
description: 'Telegram Bot secret',
example: 'z7WnDUfuhtDCBjX54Ks5vB4SAdGmdzwRVlGQjWBt',
info: 'https://core.telegram.org/bots/api#setwebhook',
hint: 'https://github.com/evanx/random-base56'
},
token: {
description: 'Telegram Bot token',
example: '243751977:AAH-WYXgsiZ8XqbzcqME7v6mUALxjktvrQc',
info: 'https://core.telegram.org/bots/api#authorizing-your-bot',
hint: 'https://telegram.me/BotFather'
},
admin: {
description: 'Authoritative Telegram username i.e. bootstrap admin user',
example: 'evanxsummers',
info: 'https://telegram.org'
},
hubRedis: {
required: false,
description: 'Remote redis for bot messages, especially for development',
example: 'redis://localhost:6333',
info: 'https://github.com/evanx/webhook-push'
}
};
The config
is populated from environment variables as follows:
const configKeys = [];
const missingConfigKeys = [];
const config = Object.keys(configMeta)
.concat(Object.keys(configDefault))
.reduce((config, key) => {
if (process.env[key]) {
assert(process.env[key] !== '', key);
config[key] = process.env[key];
configKeys.push(key);
} else if (configFile && configFile[key]) {
config[key] = configFile[key];
configKeys.push(key);
} else if (!configDefault[key] && configMeta[key].required !== false) {
missingConfigKeys.push(key);
}
return config;
}, configDefault);
where we check that an environment variable is not empty, for safety sake.
If we start the service with missing config via environment variables, the following help is printed:
domain e.g. 'authdemo.webserva.com'
"HTTPS web domain to auth access"
bot e.g. 'ExAuthDemoBot'
"Telegram Bot name i.e. this authbot"
see https://core.telegram.org/bots/api
see https://telegram.me/BotFather
secret e.g. 'z7WnDUfuhtDCBjX54Ks5vB4SAdGmdzwRVlGQjWBt'
"Telegram Bot secret"
see https://core.telegram.org/bots/api#setwebhook
see https://github.com/evanx/random-base56
token e.g. '243751977:AAH-WYXgsiZ8XqbzcqME7v6mUALxjktvrQc'
"Telegram Bot token"
see https://core.telegram.org/bots/api#authorizing-your-bot
see https://telegram.me/BotFather
admin e.g. 'evanxsummers'
"Authoritative Telegram username i.e. bootstrap admin user"
see https://telegram.org
hubRedis e.g. 'redis://localhost:6333'
"Remote hub for bot messages via Redis, especially for development"
see https://github.com/evanx/webhook-push
Also it prints a npm start
CLI using the example
config properties:
domain='authdemo.webserva.com' \
bot='ExAuthDemoBot' \
secret='z7WnDUfuhtDCBjX54Ks5vB4SAdGmdzwRVlGQjWBt' \
token='243751977:AAH-WYXgsiZ8XqbzcqME7v6mUALxjktvrQc' \
admin='evanxsummers' \
hubRedis='redis://localhost:6333' \
npm start
where this help is generated from configMeta
Let's build our application container:
docker build -t authbot https://github.com/evanx/authbot.git
where the image is named and tagged as authbot
Notice that the default Dockerfile
is as follows:
FROM mhart/alpine-node
ADD package.json .
RUN npm install
ADD index.js .
ENV port 8080
EXPOSE 8080
CMD ["node", "--harmony", "index.js"]
npm start
with missing configs will print help including for Docker run, which you must edit for your environment
i.e. with your own domain, username, bot name, token, secret etc:
docker run \
--name authbot_test -d \
--network host \
-e NODE_ENV=test \
-e domain='' \
-e bot='' \
-e secret='' \
-e token='' \
-e admin='evanxsummers' \
authbot
This tested on Docker 1.12 (Ubuntu 16.04) and 1.11 (Amazon Linux 2016.09)
docker -v
Docker version 1.12.1, build 23cf638
Docker version 1.11.2, build b9f10c9/1.11.2
cat /etc/issue
Ubuntu 16.04.1 LTS
Amazon Linux AMI release 2016.09
In this example we create an isolated network:
docker network create --driver bridge authbot_network
We can create a Redis container named redis_authbot
as follows
docker run --network=authbot_network --name redis_authbot -d redis
We query its IP number and store in shell environment variable redisHost
redisHost=`docker inspect \
--format '{{ .NetworkSettings.Networks.authbot_network.IPAddress }}' redis_authbot`
echo $redisHost
which we check that set e.g. to 172.18.0.2
Finally we run our service container:
docker run --network=authbot_network --name authbot_test -d -p 8080 \
-e NODE_ENV=test \
-e redisHost=$redisHost \
-e domain='' \
-e bot='' \
-e secret='' \
-e token='' \
-e admin='' \
authbot
where we configure redisHost
as the redis_authbot
container.
Note that we:
- use the
redis
isolated network bridge for theredis_authbot
container - name this container
authbot_test
- use the previously built image
authbot:test
Get its IP address:
address=`
docker inspect --format '{{ .NetworkSettings.Networks.authbot_network.IPAddress }}' authbot_test
`
echo $address
That address is set on the API gateway e.g. Nginx, to route HTTP requests for the domain and /authbot
location to the bot.
In a development environment on the cloud interacting with a test bot, it is useful to watch index.js
for changes and restart.
See https://github.com/evanx/authbot/blob/master/docs/auto-restart.md