forked from Telodendria/Telodendria
Apply #64: Registration tokens.
This commit is contained in:
parent
76bfa120ee
commit
ae97d8116c
8 changed files with 495 additions and 15 deletions
10
TODO.txt
10
TODO.txt
|
@ -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
251
src/RegToken.c
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
82
src/Routes/RouteTokenValid.c
Normal file
82
src/Routes/RouteTokenValid.c
Normal 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;
|
||||
}
|
26
src/Uia.c
26
src/Uia.c
|
@ -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
70
src/include/RegToken.h
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue