{"id":38337600,"url":"https://github.com/fmitra/authenticator","last_synced_at":"2026-01-17T02:57:39.291Z","repository":{"id":57550374,"uuid":"180799167","full_name":"fmitra/authenticator","owner":"fmitra","description":"User authentication through WebAuthn, TOTP, Email, and SMS","archived":false,"fork":false,"pushed_at":"2021-01-16T19:28:56.000Z","size":818,"stargazers_count":9,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-06-19T05:42:39.940Z","etag":null,"topics":["authentication","authentication-backend","fido","fido2","login","signup","totp","webauthn"],"latest_commit_sha":null,"homepage":"https://auth.7965a66.com","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fmitra.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-04-11T13:33:52.000Z","updated_at":"2024-06-12T02:21:07.000Z","dependencies_parsed_at":"2022-09-01T08:40:18.044Z","dependency_job_id":null,"html_url":"https://github.com/fmitra/authenticator","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/fmitra/authenticator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fmitra%2Fauthenticator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fmitra%2Fauthenticator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fmitra%2Fauthenticator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fmitra%2Fauthenticator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fmitra","download_url":"https://codeload.github.com/fmitra/authenticator/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fmitra%2Fauthenticator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28492593,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T02:39:23.645Z","status":"ssl_error","status_checked_at":"2026-01-17T02:34:19.649Z","response_time":85,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","authentication-backend","fido","fido2","login","signup","totp","webauthn"],"created_at":"2026-01-17T02:57:39.210Z","updated_at":"2026-01-17T02:57:39.283Z","avatar_url":"https://github.com/fmitra.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Build](https://github.com/fmitra/authenticator/workflows/Build/badge.svg) [![Report Card](https://goreportcard.com/badge/github.com/fmitra/authenticator)](https://goreportcard.com/badge/github.com/fmitra/authenticator) [![codecov](https://codecov.io/gh/fmitra/authenticator/branch/master/graph/badge.svg?token=MRMXT9NJI3)](https://codecov.io/gh/fmitra/authenticator)\n\n# authenticator\n\nA generic user authentication service supporting FIDO U2F, TOTP, Email, and SMS.\n\n## Contents\n\n* [Overview](#overview)\n\n  * [Authentication Tokens](#authentication-tokens)\n  * [Passwordless Authentication](#passwordless-authentication)\n  * [Registration](#registration)\n  * [Authentication and 2FA](#authentication)\n  * [Client Flow/Storage](#client)\n  * [Revocation/Invalidation](#revocation)\n  * [Auditability](#auditability)\n  * [Design Rationale](#rationale)\n  * [Components](#components)\n\n* [Development](#development)\n\n  * [Getting Started](#getting-started)\n  * [Test and Lint](#test-and-lint)\n  * [Load Testing](#load-testing)\n\n* [Performance](#performance)\n* [Alternatives](#alternatives)\n* [References](#references)\n* [Pending](#pending)\n\n## \u003ca name=\"overview\"\u003eOverview\u003c/a\u003e\n\nAccount management is one of the more boring and yet necessary portions of most user\nfacing systems. Here we attempt to provide some sane, secure defaults so you can\nfocus on building your product instead.\n\nFor an overview of the API, refer to the [documentation here](docs/api_v1.md)\n\nFor an example clientside implementation of some of the core API's provided here,\nrefer to the [client repository](https://github.com/fmitra/authenticator-client).\n\nAlthough there are [missing features](#pending), the most noteworthy of which is password reset,\nthis project is fully functional. It was originally written as an opportunity to\nexplore the recent addition of the Webauthn browser spec and snowballed into\na fully featured authenticator under the premise that it could one day be used\nfor future hobby projects.\n\n### \u003ca name=\"authentication-tokens\"\u003eAuthentication Tokens\u003c/a\u003e\n\n[JWT tokens](https://jwt.io) are used for authentication. Their stateless nature allows\nus to check verification without managing a session in a database. Additionally signed\ntokens provide data integrity, providing our other applications a degree of trust\nwith the user identity information contained within it.\n\nJWT Tokens are embeded with a fingerprint, which we refer to here as a `client ID` to help prevent [token sidejacking](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/JSON_Web_Token_Cheat_Sheet_for_Java.md#token-sidejacking).\n\nJWT Tokens are short lived by default (20 minutes) but may be refreshed after expiry\nwith a valid `refresh token`. Refresh tokens have their own, configurable long lived expiry time\n(15 days by default) and set on the client securely along side the client ID.\n\n### \u003ca name=\"passwordless-authentication\"\u003ePasswordless Authentication\u003c/a\u003e\n\nPasswordless authentication is **planned** as an optional system wide configuration. It is often used\nto ease onboarding flows. Popular examples can be seen by popular start ups such as\nUber, Grab, and Square Cash.  We support this this as we [can argue](https://auth0.com/passwordless) that randomly\ngenerated, time sensitive multi-character codes are oftentimes more secure then common\nuser generated passwords and mitigates password reuse.\n\n### \u003ca name=\"registration\"\u003eRegistration\u003c/a\u003e\n\nRegistration requires either a phone number or email address as it is a requirement to\nverify the authenticity of a user. The service may be required to enforce email only\nregistration, phone only, or a combination of both.\n\n### \u003ca name=\"authentication\"\u003eAuthentication and 2FA\u003c/a\u003e\n\nAuthentication requires a password (unless passwordless authentication is enabled) and\nan assertion of identity. The assertion may be one of the following 2FA methods:\n\n* **OTP**: A one time password (by default, a 6 digit code) will be delivered to the\nuser's email address or phone number. This is the default setting and may be disabled\nafter the user enables an alternative 2FA method.\n\n* **TOTP**: Users may generate a time based one time password through a supported\napplication.\n\n* **FIDO** Users may submit a signed WebAuthn challenge to authenticate with any standard\nFIDO device (e.g. MacOS fingerprint reader, YubiKey)\n\n### \u003ca name=\"client\"\u003eClient Flow/Storage\u003c/a\u003e\n\nWhile secure cookie storage is available on web browsers, tokens are instead expected\nto be stored in either LocalStorage or SessionStorage (or some secure storage in a native\nmobile app). This ensure clients have access to the token to retrieve basic information\nabout the user's idenitty. To authenticate, clients are expected to create an authentication\nheader and pass their tokens with a `Bearer` prefix. This eliminates the complexity of\nadditionally supporting CSRF tokens.\n\nTo mitigate XSS attacks targeted at token storages, tokens are fingerprinted with a random\nstring's hash. The hash value (the `client ID`) should be securely stored on the client. After\nauthentication, the client ID is set as a secure cookie on the browser. It is additionally\navailable as part of the response payload so mobile clients can store it themselves.\n\n### \u003ca name=\"revocation\"\u003eRevocation and Invalidation\u003c/a\u003e\n\nOlder tokens may be explicitly revoked by a user or automatically invalidated by us\n(for example, when refreshing a token). Handling this is an inherent problem with\nJWT tokens as revocation typlically relies on setting a short enough expiry period\nfor service owners to consider the risk minimal. This allows us to make use of one\nof the major benefits of JWT tokens by allowing simple validation without a session store.\n\nFor this project, I've opted to take the middle ground and support revocation using\na fast storage (here Redis is used) and maintaing a [blacklist](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#blacklist-storage) of Token IDs.\n\n### \u003ca name=\"auditability\"\u003eAuditability\u003c/a\u003e\n\nRecords for login history are created upon each successful login and associated with a\nJWT token ID. This history allows us to provide users a way audit their account and\nrevoke tokens. After revocations, tokens may no longer refresh and the user must login in\nagain to retrieve a new JWT token and accompanying refresh token.\n\n### \u003ca name=\"rationale\"\u003eDesign Rationale\u003c/a\u003e\n\n**Token storage**: We avoid setting authentication tokens to cookies to avoid the need to\nprovide CSRF token support and allow us to rely solely on the contents of a JWT token\nfor authenticaiton. [Fingerprinting](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#how-to-prevent_1) the token with a securely stored value is instead\nused to mitigate risks of XSS attacks that may occur by allowing clients to save their\ntokens in other storages. It's use case is similar but allows us to complete validation\nwithout storing the additional token.\n\n**Token invalidation**: While not typical in JWT support, we support token invalidation\nas it provides an additional layer of security and allows us to manage OTP codes without\npersisting them to a DB as we can now use the token as a transport mechanism for the OTP\ncode (OTP hashes are embeded in the token). The cost to support invalidation was shown\nto increase validation time by around `3ms`.\n\n**OTP Message delivery**: OTP codes may be delivered through email or SMS. SMS uses\nthe [Twilio API](./internal/twilio/twilio.go) however any other API wrapper that is set up to adhere to the same interface may\nbe swapped in. Email delivery may be completed through [Sendgrid](./internal/sendgrid/sendgrid.go) or Go's standard `net/smtp` library.\nBecause OTP codes are short lived, and users may request new codes on delivery failure,\nthey are only stored in an [in-memory queue](./internal/msgrepo/service.go) during sending as it is acceptable for messages\nto be lost (e.g. application is restarted) with no attempts made to re-send it. We validate\nOTP codes by comparing it to an embeded hash in each JWT token. The generation of a new token\nautomatically invalidates an old token with an embeded OTP hash.\n\n**2FA**: Device 2FA via a valid FIDO U2F device (through Webauthn API) is set as\nthe default 2FA method when enabled, followed by TOTP code generation and finally delivery\nvia Email or SMS. To maintain usability, we do not automatically disable one 2FA option\nwhen another is enabled. If users desires to disable a less secure method after enabling\na new 2FA method, they are expected to explicitly disable it themsleves. The Client UI\nshould budget for this and guide users through this flow.\n\n**SRP**: [SRP](https://github.com/fmitra/srp) is an authentication protocol to mitigate MITM attacks.\nIt was left out as an authentication protocol for this service as it would add significant\ncomplexity to client side auth flow  and competes with building adoption for WebAuthn.\n\n### \u003ca name=\"components\"\u003eComponents\u003c/a\u003e\n\n* PostgreSQL: Storage for users, login history, authorized FIDO devices\n* Redis: Blacklist for invalidated tokens, Webauthn session management, API ratelimiting\n* Twilio API: OTP code delivery via SMS\n* Sendgrid API: OTP code delivery via Email (optional)\n* Go stdlib net/smtp: OTP code delivery via Email (default)\n\n## \u003ca name=\"development\"\u003eDevelopment\u003c/a\u003e\n\n### \u003ca name=\"getting-started\"\u003eGetting Started\u003c/a\u003e\n\nIn order to complete send OTP codes through SMS or email, you will need a [Twilio](https://www.twilio.com/)\nAPI key as well as either email credentials to be used with Go's `net/smtp` library or\na [Sendgrid](https://sendgrid.com/) API key.\n\n**1. Generate default config**\n\n`config.json` and a corresponding `docker-compose.yml` file will be created. It assumes\nyou intend to run the client and backend on `authenticator.local`. Once you have a config file,\nmake any necessary changes. The update any necessary  (e.g. add API keys).\n\n```\ncd authenticator\nmake dev\n```\n\n**2. Start the project and dependencies**\n\nBy default the project will be exposed on port `8081`.\n\n```\ncd authenticator\ndocker-compose -f docker-compose.stage.yml up -d\n```\n\nYou can check the project is up and running via the healthcheck endpoint\n\n```\ncurl http://localhost:8081/healthcheck\n```\n\nIf you would like to build and run the project without docker, you can compile\nthe binary directly and pass the location of your configuration file:\n\n```\ngo build ./cmd/api\n./api --config=./config.json\n```\n\n**3. Setup and access the database**\n\nIf this is your first time running the project, you'll need to set up the initial\nDB schema found in [schema.go](./schema.go). You can connect with `psql` through\nthe `docker-compose` to create the tables specified in `schema.go`.\n\n```\ndocker-compose exec postgres psql -U auth -d authenticator_test\n```\n\n### \u003ca name=\"test-and-lint\"\u003eTest and Lint\u003c/a\u003e\n\nMake sure [golangci-lint](https://golangci-lint.run/usage/install/) is installed prior to running the linter.\n\n```\ndocker-compose -f docker-compose.test.yml\nmake test\nmake lint\n```\n\n### \u003ca name=\"load-testing\"\u003eLoad Testing\u003c/a\u003e\n\n[Artillery.io](https://artillery.io/docs/) is used for load testing. In depth tests\nare not set up yet but we can get a general idea of performance on [token validation](./loadtest/token-verify.yml)\nwith a Redis backed throttle.\n\nFirst install Artillery ([Node.js](https://nodejs.org) is a prerequisite)\n\n```\nnpm install -g artillery\nartillery -V\n```\n\nSet the target domain and run the tests.\n\n```\nexport AUTHENTICATOR_DOMAIN=http://138.65.75.135\nartillery run loadtest/token-verify.yml\n```\n\n## \u003ca name=\"performance\"\u003ePerformance\u003c/a\u003e\n\nAn indepth review has not been completed yet. Although an initial test on a *Digital Ocean\ndroplet $5 droplet (1GB/1CPU, 25GB SSD) with PostgreSQL and Redis running together on the same\ninstance* for token validation shows we can reasonably expect handle around `200` concurrent\nrequests per second while maintaining a response time of around `300ms` for end users for 95% of\nrequests on the single DO instance.\n\nRamping up to `800` concurrent users per second on the same DO instance over a 7 minute period\nshows degregation in response times to `500ms` for 95% of requests with a `0.004%` error rate.\n\nExample report overview:\n\n* Server: Digital Ocean (1GB/1CPU, 25GB SSD)\n* Server Location: New York\n* Load Test Client Location: New York\n* Environment: Application, PostgreSQL, Redis, running dockerized on the single instance\n* Conditions: Maximum 800req/sec, average 167req/sec over 7 minutes\n\nReport: Average 5req/sec\n\n```\nReport @ 14:43:47(-0400) 2020-08-07\nElapsed time: 1 minute, 10 seconds\n  Scenarios launched:  56\n  Scenarios completed: 56\n  Requests completed:  56\n  Mean response/sec: 5.61\n  Response time (msec):\n    min: 252.3\n    max: 288.3\n    median: 267.8\n    p95: 283.9\n    p99: 288.3\n  Codes:\n    401: 10\n    429: 46\n```\n\nReport: Average 200req/sec\n\n```\nReport @ 14:59:38(-0400) 2020-08-07\nElapsed time: 5 minutes, 20 seconds\n  Scenarios launched:  2430\n  Scenarios completed: 2420\n  Requests completed:  2420\n  Mean response/sec: 243.34\n  Response time (msec):\n    min: 253.3\n    max: 1729.4\n    median: 278.9\n    p95: 320.3\n    p99: 727.5\n  Codes:\n    401: 10\n    429: 2410\n```\n\nReport: Request ramp to 800req/sec\n\n```\nReport @ 15:01:18(-0400) 2020-08-07\nElapsed time: 7 minutes, 0 seconds\n  Scenarios launched:  7045\n  Scenarios completed: 7009\n  Requests completed:  7009\n  Mean response/sec: 705.91\n  Response time (msec):\n    min: 266.4\n    max: 3362.7\n    median: 359.3\n    p95: 507.6\n    p99: 1585.1\n  Codes:\n    401: 10\n    429: 6999\n```\n\nReport: Summary\n\n```\nSummary report @ 15:01:23(-0400) 2020-08-07\n  Scenarios launched:  71063\n  Scenarios completed: 71060\n  Requests completed:  71060\n  Mean response/sec: 167.23\n  Response time (msec):\n    min: 250.1\n    max: 4032.2\n    median: 296.2\n    p95: 496.3\n    p99: 1529.3\n  Scenario counts:\n    0: 71063 (100%)\n  Codes:\n    401: 425\n    429: 70635\n  Errors:\n    ECONNRESET: 3\n```\n\n## \u003ca name=\"alternatives\"\u003eAlternatives\u003c/a\u003e\n\n* [Auth0](https://auth0.com/) Packages authentication as a service but results in leaving\nyour user account management up to an external third party which may not be feasible\nfor some compliance or business needs. It's free plan provides limited support and paid\nplans are arguably expensive for several thousands of users.\n\n* [AuthRocket](https://authrocket.com) Provides similar features to Auth0. The service\nis less mature than it's competitor and more expensive at low tier plans.\n\n## \u003ca name=\"references\"\u003eReferences\u003c/a\u003e\n\n* [JWT Token Revokation](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#no-built-in-token-revocation-by-the-user)\n\n* [Token Sidejacking](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#token-sidejacking)\n\n* [Passwordless Authentication](https://auth0.com/passwordless)\n\n* [Token refresh](https://auth0.com/learn/refresh-tokens)\n\n## \u003ca name=\"pending\"\u003ePending\u003c/a\u003e\n\nFollowing features are still pending and will be implemented in order:\n\n* **Password reset:** Password reset will be inspired by the advice provided from [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html).\nUsers will be required to complete 2FA, after which a password reset email will be\nsent with a time sensitive code to change their password.\n\n* **User enumeration:** User enumeration prevention can be improved once password reset is enabled.\nUser lookup errors on the signup flow should trigger the 2FA step on password reset. This strategy\nis currently employed by Facebook (as of 2020). At the moment we simply return a generic error.\n\n* **Passwordless authentication:** As discussed above in this document, passwordless authentication\nis one of the planned features and will be implemented as an optional system wide configuration.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffmitra%2Fauthenticator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffmitra%2Fauthenticator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffmitra%2Fauthenticator/lists"}