Apply #64: Registration tokens.

This commit is contained in:
Jordan Bancino 2023-03-14 00:37:24 +00:00
parent 76bfa120ee
commit ae97d8116c
8 changed files with 495 additions and 15 deletions

View file

@ -44,11 +44,11 @@ Milestone: v0.3.0
[ ] /_telodendria/admin/config endpoint
[ ] Refactor TelodendriaConfig to just Config (ConfigLock() and ConfigUnlock())
[ ] Client-Server API
[ ] 4: Token-based user registration
[ ] Implement user-interactive auth flow
[ ] Token validity endpoint
[ ] Add m.login.registration_token to registration endpoint
[~] Client-Server API
[x] 4: Token-based user registration
[x] Implement user-interactive auth flow
[x] Token validity endpoint
[x] Add m.login.registration_token to registration endpoint
flow
- Ensure that registration tokens can be used even if
registration is disabled.

251
src/RegToken.c Normal file
View file

@ -0,0 +1,251 @@
/*
* Copyright (C) 2022-2023 Jordan Bancino <@jordan:bancino.net>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <RegToken.h>
#include <string.h>
#include <ctype.h>
#include <Memory.h>
#include <Json.h>
#include <Util.h>
#include <Str.h>
int
RegTokenValid(RegTokenInfo * token)
{
HashMap *tokenJson;
int uses, used;
unsigned long expiration;
if (!token || !RegTokenExists(token->db, token->name))
{
return 0;
}
tokenJson = DbJson(token->ref);
uses = JsonValueAsInteger(HashMapGet(tokenJson, "uses"));
used = JsonValueAsInteger(HashMapGet(tokenJson, "used"));
expiration = JsonValueAsInteger(HashMapGet(tokenJson, "expires_on"));
return (!expiration || (UtilServerTs() <= expiration)) &&
(uses == -1 || used < uses);
}
void
RegTokenUse(RegTokenInfo * token)
{
HashMap *tokenJson;
if (!token || !RegTokenExists(token->db, token->name))
{
return;
}
if (token->uses >= 0 && token->used >= token->uses)
{
return;
}
token->used++;
/* Write the information to the hashmap */
tokenJson = DbJson(token->ref);
JsonValueFree(HashMapSet(tokenJson, "used", JsonValueInteger(token->used)));
}
int
RegTokenExists(Db * db, char *token)
{
if (!token || !db)
{
return 0;
}
return DbExists(db, 3, "tokens", "registration", token);
}
int
RegTokenDelete(RegTokenInfo * token)
{
if (!token || !RegTokenClose(token))
{
return 0;
}
if (!DbDelete(token->db, 3, "tokens", "registration", token->name))
{
return 0;
}
Free(token->name);
Free(token->owner);
Free(token);
return 1;
}
RegTokenInfo *
RegTokenGetInfo(Db * db, char *token)
{
RegTokenInfo *ret;
DbRef *tokenRef;
HashMap *tokenJson;
if (!RegTokenExists(db, token))
{
return NULL;
}
tokenRef = DbLock(db, 3, "tokens", "registration", token);
if (!tokenRef)
{
return NULL;
}
tokenJson = DbJson(tokenRef);
ret = Malloc(sizeof(RegTokenInfo));
ret->db = db;
ret->ref = tokenRef;
ret->owner =
StrDuplicate(JsonValueAsString(HashMapGet(tokenJson, "created_by")));
ret->name = StrDuplicate(token);
ret->expires =
JsonValueAsInteger(HashMapGet(tokenJson, "expires_on"));
ret->created =
JsonValueAsInteger(HashMapGet(tokenJson, "created_on"));
ret->uses =
JsonValueAsInteger(HashMapGet(tokenJson, "uses"));
ret->used =
JsonValueAsInteger(HashMapGet(tokenJson, "used"));
return ret;
}
void
RegTokenFree(RegTokenInfo * tokeninfo)
{
if (tokeninfo)
{
Free(tokeninfo->name);
Free(tokeninfo->owner);
Free(tokeninfo);
}
}
int
RegTokenClose(RegTokenInfo * tokeninfo)
{
if (!tokeninfo)
{
return 0;
}
return DbUnlock(tokeninfo->db, tokeninfo->ref);
}
static int
RegTokenVerify(char *token)
{
size_t i, size;
char c;
if (!token)
{
return 0;
}
/* The spec says the following: "The token required for this
* authentication [...] is an opaque string with maximum length of
* 64 characters in the range [A-Za-z0-9._~-]." */
if ((size = strlen(token)) > 64)
{
return 0;
}
for (i = 0; i < size; i++)
{
c = token[i];
if (!(isalnum(c) || c == '0' || c == '_' || c == '~' || c == '-'))
{
return 0;
}
}
return 1;
}
RegTokenInfo *
RegTokenCreate(Db * db, char *name, char *owner, unsigned long expires, int uses)
{
RegTokenInfo *ret;
HashMap *tokenJson;
unsigned long timestamp = UtilServerTs();
if (!db || !name || !owner)
{
return NULL;
}
/* -1 indicates infinite uses; zero and all positive values are a valid
* number of uses; althought zero would be rather useless. Anything less
* than -1 doesn't make sense. */
if (uses < -1)
{
return NULL;
}
/* Verify the token */
if (!RegTokenVerify(name) || (expires > 0 && expires < timestamp))
{
return NULL;
}
ret = Malloc(sizeof(RegTokenInfo));
/* Set the token's properties */
ret->db = db;
ret->ref = DbCreate(db, 3, "tokens", "registration", name);
if (!ret->ref)
{
/* RegToken already exists or some weird fs error */
Free(ret);
return NULL;
}
ret->name = StrDuplicate(name);
ret->owner = StrDuplicate(owner);
ret->used = 0;
ret->uses = uses;
ret->created = timestamp;
ret->expires = expires;
/* Write user info to database. */
tokenJson = DbJson(ret->ref);
HashMapSet(tokenJson, "created_by",
JsonValueString(ret->owner));
HashMapSet(tokenJson, "created_on",
JsonValueInteger(ret->created));
HashMapSet(tokenJson, "expires_on",
JsonValueInteger(ret->expires));
HashMapSet(tokenJson, "used",
JsonValueInteger(ret->used));
HashMapSet(tokenJson, "uses",
JsonValueInteger(ret->uses));
return ret;
}

View file

@ -109,10 +109,50 @@ ROUTE_IMPL(RouteMatrix, args)
Free(pathPart);
return response;
}
else if (MATRIX_PATH_EQUALS(pathPart, "v1"))
{
/* TODO: This *really* does not look good. */
Free(pathPart);
pathPart = MATRIX_PATH_POP(args->path);
if (MATRIX_PATH_EQUALS(pathPart, "register"))
{
Free(pathPart);
pathPart = MATRIX_PATH_POP(args->path);
if (MATRIX_PATH_EQUALS(pathPart, "m.login.registration_token"))
{
Free(pathPart);
pathPart = MATRIX_PATH_POP(args->path);
if (MATRIX_PATH_EQUALS(pathPart, "validity"))
{
Free(pathPart);
response = RouteTokenValid(args);
}
else
{
Free(pathPart);
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
return MatrixErrorCreate(M_NOT_FOUND);
}
}
else
{
Free(pathPart);
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
return MatrixErrorCreate(M_NOT_FOUND);
}
}
else
{
Free(pathPart);
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
return MatrixErrorCreate(M_NOT_FOUND);
}
}
else
{
Free(pathPart);
HttpResponseStatus(args->context, HTTP_NOT_FOUND);
return MatrixErrorCreate(M_NOT_FOUND);
}
return response;
}

View file

@ -33,6 +33,21 @@
#include <User.h>
#include <Uia.h>
static Array *
RouteRegisterRegFlow(void)
{
Array *response = ArrayCreate();
if (!response)
{
return NULL;
}
ArrayAdd(response, UiaStageBuild("m.login.registration_token", NULL));
return response;
}
ROUTE_IMPL(RouteRegister, args)
{
HashMap *request = NULL;
@ -73,13 +88,6 @@ ROUTE_IMPL(RouteRegister, args)
return MatrixErrorCreate(M_NOT_JSON);
}
if (!(args->matrixArgs->config->flags & TELODENDRIA_REGISTRATION))
{
HttpResponseStatus(args->context, HTTP_FORBIDDEN);
response = MatrixErrorCreate(M_FORBIDDEN);
goto finish;
}
val = HashMapGet(request, "username");
if (val)
{
@ -107,9 +115,12 @@ ROUTE_IMPL(RouteRegister, args)
}
uiaFlows = ArrayCreate();
ArrayAdd(uiaFlows, UiaDummyFlow());
ArrayAdd(uiaFlows, RouteRegisterRegFlow());
/* TODO: Add registration token flow */
if (args->matrixArgs->config->flags & TELODENDRIA_REGISTRATION)
{
ArrayAdd(uiaFlows, UiaDummyFlow());
}
uiaResult = UiaComplete(uiaFlows, args->context,
args->matrixArgs->db, request, &response,

View file

@ -0,0 +1,82 @@
/*
* Copyright (C) 2022-2023 Jordan Bancino <@jordan:bancino.net>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <Routes.h>
#include <string.h>
#include <RegToken.h>
#include <Json.h>
#include <HashMap.h>
#include <Str.h>
ROUTE_IMPL(RouteTokenValid, args)
{
Db *db = args->matrixArgs->db;
HashMap *response = NULL;
HashMap *request = NULL;
RegTokenInfo *info = NULL;
char *tokenstr;
if (HttpRequestMethodGet(args->context) != HTTP_GET)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_UNRECOGNIZED);
}
request = JsonDecode(HttpStream(args->context));
if (!request)
{
HttpResponseStatus(args->context, HTTP_BAD_REQUEST);
return MatrixErrorCreate(M_NOT_JSON);
}
tokenstr = JsonValueAsString(HashMapGet(request, "token"));
if (!tokenstr || !RegTokenExists(db, tokenstr))
{
response = HashMapCreate();
JsonFree(request);
HashMapSet(response, "valid", JsonValueBoolean(0));
return response;
}
info = RegTokenGetInfo(db, tokenstr);
response = HashMapCreate();
if (!RegTokenValid(info))
{
JsonFree(request);
RegTokenClose(info);
RegTokenFree(info);
HashMapSet(response, "valid", JsonValueBoolean(0));
return response;
}
RegTokenClose(info);
RegTokenFree(info);
HashMapSet(response, "valid", JsonValueBoolean(1));
JsonFree(request);
return response;
}

View file

@ -25,6 +25,7 @@
#include <string.h>
#include <RegToken.h>
#include <Memory.h>
#include <Array.h>
#include <Json.h>
@ -390,7 +391,30 @@ UiaComplete(Array * flows, HttpServerContext * context, Db * db,
}
else if (strcmp(authType, "m.login.registration_token") == 0)
{
/* TODO */
RegTokenInfo *tokenInfo;
char *token = JsonValueAsString(HashMapGet(auth, "token"));
if (!RegTokenExists(db, token))
{
HttpResponseStatus(context, HTTP_UNAUTHORIZED);
ret = BuildResponse(flows, db, response, session, dbRef);
goto finish;
}
tokenInfo = RegTokenGetInfo(db, token);
if (!RegTokenValid(tokenInfo))
{
RegTokenClose(tokenInfo);
RegTokenFree(tokenInfo);
HttpResponseStatus(context, HTTP_UNAUTHORIZED);
ret = BuildResponse(flows, db, response, session, dbRef);
goto finish;
}
/* Use the token, and then close it. */
RegTokenUse(tokenInfo);
RegTokenClose(tokenInfo);
RegTokenFree(tokenInfo);
}
else if (strcmp(authType, "m.login.recaptcha") == 0)
{

70
src/include/RegToken.h Normal file
View file

@ -0,0 +1,70 @@
/*
* Copyright (C) 2022-2023 Jordan Bancino <@jordan:bancino.net>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef TELODENDRIA_REGTOKEN_H
#define TELODENDRIA_REGTOKEN_H
#include <Db.h>
typedef struct RegTokenInfo
{
Db *db;
DbRef *ref;
char *name;
char *owner;
int used;
int uses;
unsigned long created;
unsigned long expires;
} RegTokenInfo;
extern void
RegTokenUse(RegTokenInfo *);
extern int
RegTokenExists(Db *, char *);
extern int
RegTokenDelete(RegTokenInfo *);
extern RegTokenInfo *
RegTokenGetInfo(Db *, char *);
extern RegTokenInfo *
RegTokenCreate(Db *, char *, char *, unsigned long, int);
extern void
RegTokenFree(RegTokenInfo *);
extern int
RegTokenValid(RegTokenInfo *);
extern int
RegTokenClose(RegTokenInfo *);
#endif

View file

@ -67,6 +67,8 @@ ROUTE(RouteRegister); /* /_matrix/client/(r0|v3)/register */
ROUTE(RouteRefresh); /* /_matrix/client/(r0|v3)/refresh */
ROUTE(RouteWhoami); /* /_matrix/client/(r0|v3)/whoami */
ROUTE(RouteTokenValid); /* /_matrix/client/v1/register/m.login.registration_token/validity */
#undef ROUTE
#endif