diff --git a/Cargo.lock b/Cargo.lock index b95620f..7b4da79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,6 +203,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", + "strum", "thiserror", "time 0.3.27", "tokio", @@ -5209,6 +5210,9 @@ name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" diff --git a/SDK/auth-sdk-js/.gitignore b/SDK/auth-sdk-js/.gitignore deleted file mode 100644 index 76add87..0000000 --- a/SDK/auth-sdk-js/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist \ No newline at end of file diff --git a/SDK/auth-sdk-js/package-lock.json b/SDK/auth-sdk-js/package-lock.json deleted file mode 100644 index c1b0539..0000000 --- a/SDK/auth-sdk-js/package-lock.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "auth-sdk-js", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "auth-sdk-js", - "version": "0.1.0", - "devDependencies": { - "@types/node": "^20.5.6", - "@types/react": "^18.2.21", - "react": "^18.2.0", - "typescript": "^5.2.2" - } - }, - "node_modules/@types/node": { - "version": "20.5.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.6.tgz", - "integrity": "sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==", - "dev": true - }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", - "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, - "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - } - } -} diff --git a/SDK/auth-sdk-js/package.json b/SDK/auth-sdk-js/package.json deleted file mode 100644 index 4055deb..0000000 --- a/SDK/auth-sdk-js/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "auth-sdk-js", - "version": "0.1.0", - "description": "SDK for auth", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "", - "devDependencies": { - "@types/node": "^20.5.6", - "@types/react": "^18.2.21", - "typescript": "^5.2.2" - }, - "peerDependency": { - "react": "^18.2.0" - } -} diff --git a/SDK/auth-sdk-js/src/authContext.tsx b/SDK/auth-sdk-js/src/authContext.tsx deleted file mode 100644 index 0889748..0000000 --- a/SDK/auth-sdk-js/src/authContext.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from "react"; -import { useState, ReactNode, createContext, useContext } from "react"; - -type AuthContextType = { - token: string | null; - setToken: (token: string) => void; -}; - -export const AuthContext = createContext( - undefined -); - -export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; -}; - -type AuthProviderProps = { - children: ReactNode; -}; - -export const AuthProvider = ({ children }: AuthProviderProps) => { - const [token, setToken] = useState(null); - - // TODO: Add logic to acquire tokens, refresh tokens, etc. - - return ( - - {children} - - ); -}; diff --git a/SDK/auth-sdk-js/src/index.ts b/SDK/auth-sdk-js/src/index.ts deleted file mode 100644 index 1b96542..0000000 --- a/SDK/auth-sdk-js/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./authContext"; \ No newline at end of file diff --git a/SDK/auth-sdk-js/tsconfig.json b/SDK/auth-sdk-js/tsconfig.json deleted file mode 100644 index b5dcb4c..0000000 --- a/SDK/auth-sdk-js/tsconfig.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "jsx": "react", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} diff --git a/database/AuthCore/.env b/database/AuthCore/.env new file mode 100644 index 0000000..e273e0e --- /dev/null +++ b/database/AuthCore/.env @@ -0,0 +1,2 @@ + +DATABASE_URL="postgresql://postgres:vzr3XyG4pxJK7y8D@localhost:5432/authcore" \ No newline at end of file diff --git a/proto/authcore/auth/basic.proto b/proto/authcore/auth/basic.proto new file mode 100644 index 0000000..b78e180 --- /dev/null +++ b/proto/authcore/auth/basic.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package authcore.auth.basic; + +message RegisterRequest { + string email = 3; + string password = 2; + string application_id = 4; +} + +message RegisterResponse { + string user_id = 1; +} + +enum ErrorCode { + EmailFormat = 0; + PasswordFormat = 1; + AlreadyExists = 2; + ApplicationDoesNotExist = 3; + InternalServerError = 4; +} + +service BasicAuth { + rpc Register(RegisterRequest) returns (RegisterResponse); +} \ No newline at end of file diff --git a/proto/authcore.proto b/proto/authcore/authcore.proto similarity index 69% rename from proto/authcore.proto rename to proto/authcore/authcore.proto index e530115..049ab7b 100644 --- a/proto/authcore.proto +++ b/proto/authcore/authcore.proto @@ -2,9 +2,9 @@ syntax = "proto3"; package authcore; -message GetAuthCoreVersionRequest {} +message GetVersionRequest {} -message GetAuthCoreVersionResponse { +message GetVersionResponse { string version = 1; } @@ -20,9 +20,8 @@ message DeleteApplicationRequest { message DeleteApplicationResponse {} -service AuthCorePlatform { - rpc GetAuthCoreVersion(GetAuthCoreVersionRequest) - returns (GetAuthCoreVersionResponse) {} +service Platform { + rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {} rpc AddApplication(AddApplicationRequest) returns (AddApplicationResponse) { } diff --git a/proto/authcore/error.proto b/proto/authcore/error.proto new file mode 100644 index 0000000..7613746 --- /dev/null +++ b/proto/authcore/error.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package authcore.error; + +message DetailedError { + string code = 1; + string message = 2; +} \ No newline at end of file diff --git a/proto/authcore/session.proto b/proto/authcore/session.proto new file mode 100644 index 0000000..24d4c91 --- /dev/null +++ b/proto/authcore/session.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package authcore.session; + +message ValidateRequest { + string refreshToken = 1; +} + +message ValidateResponse {} + +message InvalidateRequest { + string accessToken = 1; +} + +message InvalidateResponse {} + +service Session { + rpc Validate(ValidateRequest) returns (ValidateResponse) {} + rpc Invalidate(InvalidateRequest) returns (InvalidateResponse) {} +} \ No newline at end of file diff --git a/services/authcore/Cargo.toml b/services/authcore/Cargo.toml index 70a07ff..5261bb1 100644 --- a/services/authcore/Cargo.toml +++ b/services/authcore/Cargo.toml @@ -35,6 +35,7 @@ validator = { version = "0.16.0", features = ["derive"] } serde_repr = "0.1.12" serde_urlencoded = "0.7.1" time = "0.3.27" +strum = { version = "0.25", features = ["derive"] } [build-dependencies] tonic-build = "0.9.2" diff --git a/services/authcore/build.rs b/services/authcore/build.rs index 66e02f2..4a3404a 100644 --- a/services/authcore/build.rs +++ b/services/authcore/build.rs @@ -1,4 +1,27 @@ fn main() -> Result<(), Box> { - tonic_build::compile_protos("../../proto/authcore.proto")?; + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=Cargo.lock"); + + tonic_build::configure() + .build_client(false) + .build_server(true) + .type_attribute( + "DetailedError", + "#[derive(serde::Deserialize, serde::Serialize)]", + ) + .type_attribute( + "ErrorCode", + "#[derive(strum::Display, serde::Deserialize, serde::Serialize)]", + ) + .compile( + &[ + "authcore.proto", + "auth/basic.proto", + "session.proto", + "error.proto", + ], + &["../../proto/authcore"], + )?; + Ok(()) } diff --git a/services/authcore/src/core/basic/login.rs b/services/authcore/src/core/basic/login.rs index 84cf903..a2801e6 100644 --- a/services/authcore/src/core/basic/login.rs +++ b/services/authcore/src/core/basic/login.rs @@ -1,5 +1,4 @@ -use axum_extra::extract::cookie::{Cookie, SameSite}; -use chrono::{DateTime, Duration, Utc}; +use chrono::Duration; use crypto::snowflake::Snowflake; use thiserror::Error; @@ -59,7 +58,6 @@ pub async fn with_basic_auth( password: String, application_id: Snowflake, ip_address: String, - totp_code: Option, ) -> Result { // Get application from database. let application = match ReplicatedApplication::get(prisma_client, application_id).await { @@ -105,33 +103,13 @@ pub async fn with_basic_auth( return Err(BasicLoginError::WrongCredentials); } - // Check if user has 2FA through TOTP enabled. - let totp = user.totp(); - // If user does not have 2FA enabled, return the user. - if totp.is_none() { - return Ok(user); - } - - // If user has 2FA enabled, check if user has 2FA through TOTP enabled. - let totp = totp.unwrap(); - - // If the user provided a TOTP code, check if it is correct. - if let Some(totp_code) = totp_code { - let totp_result = totp.verify(prisma_client, totp_code).await; - if totp_result.is_err() || !totp_result.unwrap() { - return Err(BasicLoginError::Wrong2FA); - } - } else { - // If the user did not provide a TOTP code, return the user id. + if user.totp().is_some() { return Err(BasicLoginError::NeedFurtherVerificationThrough2FA( Box::new(user), )); } - // Check if user has 2FA through U2F enabled. - - // Set the last login time and ip address. prisma_client .user() .update( @@ -175,15 +153,3 @@ pub async fn create_refresh_and_access_token( Ok((refresh_token, access_token)) } - -pub fn create_refresh_cookie<'a>(token: String, expire: DateTime) -> Cookie<'a> { - let expiration_time = time::OffsetDateTime::from_unix_timestamp(expire.timestamp()).unwrap(); - - Cookie::build("refresh", token) - .secure(false) // TODO: set to true - .http_only(true) - .expires(expiration_time) - .path("/") - .same_site(SameSite::Strict) - .finish() -} diff --git a/services/authcore/src/core/basic/register.rs b/services/authcore/src/core/basic/register.rs index 9ce2737..ce3d0c3 100644 --- a/services/authcore/src/core/basic/register.rs +++ b/services/authcore/src/core/basic/register.rs @@ -10,24 +10,19 @@ use crate::{ #[derive(Debug, Error)] pub enum BasicRegistrationError { #[error("invalid email address")] - InvalidEmailAddressFormat, - #[error("email address already exists")] - EmailAddressAlreadyExists, + EmailFormat, + #[error("invalid password")] - InvalidPassword(Vec), - #[error("invalid username")] - InvalidUsernameFormat, - #[error("username already exists")] - UsernameAlreadyExists, + PasswordFormat(Vec), + + #[error("email address already exists")] + AlreadyExists, #[error("application does not exist")] ApplicationDoesNotExist, - #[error("database error")] - QueryError(#[from] prisma_client_rust::QueryError), - - #[error("unknown error")] - Unknown, + #[error("internal server error")] + InternalServerError, } pub struct BasicRegistrationData { @@ -50,17 +45,17 @@ pub struct BasicRegistrationData { pub async fn with_basic_auth( state: &AppState, mut data: BasicRegistrationData, -) -> Result<(), BasicRegistrationError> { +) -> Result { // validate email if !crypto::input::email::validate_email(&data.email) { - return Err(BasicRegistrationError::InvalidEmailAddressFormat); + return Err(BasicRegistrationError::EmailFormat); } // check if email already exists match User::find_by_email(state.prisma(), &data.email, data.application_id, vec![]).await { - Ok(_) => return Err(BasicRegistrationError::EmailAddressAlreadyExists), + Ok(_) => return Err(BasicRegistrationError::AlreadyExists), Err(ModelError::NotFound) => (), - _ => return Err(BasicRegistrationError::Unknown), + _ => return Err(BasicRegistrationError::InternalServerError), } // validate password @@ -99,7 +94,7 @@ pub async fn with_basic_auth( if let Err(e) = password::validate_password(&data.password, &user_inputs, true, password_requirements) { - return Err(BasicRegistrationError::InvalidPassword(e)); + return Err(BasicRegistrationError::PasswordFormat(e)); } // create user builder @@ -123,7 +118,7 @@ pub async fn with_basic_auth( // add password to builder let password_hash = crypto::password::hash_and_salt_password(&data.password); if password_hash.is_err() { - return Err(BasicRegistrationError::Unknown); + return Err(BasicRegistrationError::InternalServerError); } // Set the password hash @@ -138,12 +133,12 @@ pub async fn with_basic_auth( return Err(BasicRegistrationError::ApplicationDoesNotExist); } - return Err(BasicRegistrationError::QueryError(e)); + return Err(BasicRegistrationError::InternalServerError); } - _ => return Err(BasicRegistrationError::Unknown), + _ => return Err(BasicRegistrationError::InternalServerError), }; info!("user created: {:#?}", user); - Ok(()) + Ok(user) } diff --git a/services/authcore/src/core/token.rs b/services/authcore/src/core/token.rs index baa48f0..4301f65 100644 --- a/services/authcore/src/core/token.rs +++ b/services/authcore/src/core/token.rs @@ -1,123 +1,5 @@ -use chrono::{DateTime, Utc}; -use crypto::{ - snowflake::Snowflake, - tokens::paseto::{self, DefaultClaims, OwnedClaims}, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; +mod access; +mod refresh; -use crate::{ - models::{error::ModelError, prisma::UserTokenType, user::UserToken, PrismaClient}, - state::AppState, -}; - -#[derive(Debug, Error)] -pub enum RefreshTokenError { - #[error("Invalid token")] - InvalidToken, - - #[error("Token expired")] - TokenExpired, - - #[error("Token revoked")] - TokenRevoked, - - #[error("internal paseto error")] - PasetoError(#[from] paseto::Error), - - #[error("database error")] - InternalDatabaseError(#[from] ModelError), - - #[error("database error")] - QueryError(#[from] prisma_client_rust::QueryError), -} - -pub async fn new_refresh_token( - state: &AppState, - prisma_client: &PrismaClient, - user_id: Snowflake, - expires_at: DateTime, - ip_address: Option, - user_agent: Option, -) -> Result { - let token_id = state.id_generator().next_snowflake().unwrap(); - - let default_claims = DefaultClaims::builder("AuthCore", expires_at, token_id) - .subject(user_id) - .build(); - - let token = paseto::encrypt_token(default_claims, state.paseto_key())?; - - let res = UserToken::builder(token_id, user_id, UserTokenType::Refresh, token, expires_at) - .ip_address(ip_address) - .user_agent(user_agent) - .build(prisma_client) - .await?; - - Ok(res) -} - -pub async fn verify_refresh_token( - state: &AppState, - prisma_client: &PrismaClient, - token: &str, -) -> Result { - // Parse the token - let claims = paseto::validate_token(token, state.paseto_key())?; - - // Validate it against the database - let token = UserToken::get( - prisma_client, - claims.subject().unwrap().to_owned().try_into().unwrap(), - claims.token_id().try_into().unwrap(), - UserTokenType::Refresh, - ) - .await?; - - if token.expires_at() < Utc::now() { - return Err(RefreshTokenError::TokenExpired); - } - - Ok(token) -} - -#[derive(Debug, Error)] -pub enum AccessTokenError { - #[error("internal paseto error")] - PasetoError(#[from] paseto::Error), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AccessTokenClaims { - refresh_token_id: Snowflake, -} - -pub fn new_access_token( - state: &AppState, - user_id: Snowflake, - expiration: DateTime, - refresh_token_id: Snowflake, -) -> Result { - // TODO: A client application should be able to define a custom paseto token layout, also be able to switch to using JWTs - let default_claims = DefaultClaims::builder( - "AuthCore", - expiration, - state.id_generator().next_snowflake().unwrap(), - ) - .subject(user_id) - .audience("AuthCore") - .not_before(Utc::now()) - .other(AccessTokenClaims { refresh_token_id }) - .build(); - - paseto::encrypt_token(default_claims, state.paseto_key()) -} - -pub fn verify_access_token( - state: &AppState, - token: &str, -) -> Result, AccessTokenError> { - let claims = paseto::validate_token(token, state.paseto_key())?; - - Ok(claims) -} +pub use access::*; +pub use refresh::*; diff --git a/services/authcore/src/core/token/access.rs b/services/authcore/src/core/token/access.rs new file mode 100644 index 0000000..5860f00 --- /dev/null +++ b/services/authcore/src/core/token/access.rs @@ -0,0 +1,50 @@ +use chrono::{DateTime, Utc}; +use crypto::{ + snowflake::Snowflake, + tokens::paseto::{self, DefaultClaims, OwnedClaims}, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::state::AppState; + +#[derive(Debug, Error)] +pub enum AccessTokenError { + #[error("internal paseto error")] + PasetoError(#[from] paseto::Error), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccessTokenClaims { + refresh_token_id: Snowflake, +} + +pub fn new_access_token( + state: &AppState, + user_id: Snowflake, + expiration: DateTime, + refresh_token_id: Snowflake, +) -> Result { + // TODO: A client application should be able to define a custom paseto token layout, also be able to switch to using JWTs + let default_claims = DefaultClaims::builder( + "AuthCore", + expiration, + state.id_generator().next_snowflake().unwrap(), + ) + .subject(user_id) + .audience("AuthCore") + .not_before(Utc::now()) + .other(AccessTokenClaims { refresh_token_id }) + .build(); + + paseto::encrypt_token(default_claims, state.paseto_key()) +} + +pub fn verify_access_token( + state: &AppState, + token: &str, +) -> Result, AccessTokenError> { + let claims = paseto::validate_token(token, state.paseto_key())?; + + Ok(claims) +} diff --git a/services/authcore/src/core/token/refresh.rs b/services/authcore/src/core/token/refresh.rs new file mode 100644 index 0000000..362d578 --- /dev/null +++ b/services/authcore/src/core/token/refresh.rs @@ -0,0 +1,137 @@ +use axum_extra::extract::{ + cookie::{Cookie, SameSite}, + CookieJar, +}; +use chrono::{DateTime, Utc}; +use crypto::{ + snowflake::Snowflake, + tokens::paseto::{self, DefaultClaims}, +}; +use thiserror::Error; + +use crate::{ + models::{error::ModelError, prisma::UserTokenType, user::UserToken, PrismaClient}, + state::AppState, +}; + +#[derive(Debug, Error)] +pub enum RefreshTokenError { + #[error("Invalid token")] + InvalidToken, + + #[error("Token expired")] + TokenExpired, + + #[error("Token revoked")] + TokenRevoked, + + #[error("internal paseto error")] + PasetoError(#[from] paseto::Error), + + #[error("database error")] + InternalDatabaseError(#[from] ModelError), + + #[error("database error")] + QueryError(#[from] prisma_client_rust::QueryError), +} + +/// Create a new refresh token. +/// # Arguments +/// * `state` - The app state. +/// * `user_id` - The user ID. +/// * `expires_at` - The expiration date. +/// * `ip_address` - The IP address. +/// * `user_agent` - The user agent. +/// +/// # Returns +/// * `Ok(UserToken)` - The user token. +/// * `Err(RefreshTokenError)` - The error. +pub async fn new_refresh_token( + state: &AppState, + prisma_client: &PrismaClient, + user_id: Snowflake, + expires_at: DateTime, + ip_address: Option, + user_agent: Option, +) -> Result { + let token_id = state.id_generator().next_snowflake().unwrap(); + + let default_claims = DefaultClaims::builder("AuthCore", expires_at, token_id) + .subject(user_id) + .build(); + + let token = paseto::encrypt_token(default_claims, state.paseto_key())?; + + let res = UserToken::builder(token_id, user_id, UserTokenType::Refresh, token, expires_at) + .ip_address(ip_address) + .user_agent(user_agent) + .build(prisma_client) + .await?; + + Ok(res) +} + +/// Verify a refresh token. +/// # Arguments +/// * `state` - The app state. +/// * `token` - The refresh token. +/// * `application_id` - The application ID. +/// +/// # Returns +/// * `Ok(UserToken)` - The user token. +/// * `Err(RefreshTokenError)` - The error. +pub async fn verify_refresh_token( + state: &AppState, + prisma_client: &PrismaClient, + token: &str, +) -> Result { + // Parse the token + let claims = paseto::validate_token(token, state.paseto_key())?; + + // Validate it against the database + let token = UserToken::get( + prisma_client, + claims.subject().unwrap().to_owned().try_into().unwrap(), + claims.token_id().try_into().unwrap(), + UserTokenType::Refresh, + ) + .await?; + + if token.expires_at() < Utc::now() { + return Err(RefreshTokenError::TokenExpired); + } + + Ok(token) +} + +pub fn create_refresh_cookie<'a>( + token: String, + expire: DateTime, + application_id: Snowflake, +) -> Cookie<'a> { + let expiration_time = time::OffsetDateTime::from_unix_timestamp(expire.timestamp()).unwrap(); + let refresh_cookie_name = build_refresh_cookie_name(application_id); + + Cookie::build(refresh_cookie_name, token) + .domain("localhost") + .secure(false) // TODO: set to true + .http_only(true) + .expires(expiration_time) + .path("/") + .same_site(SameSite::Strict) + .finish() +} + +pub fn get_refresh_cookie(jar: &CookieJar, application_id: Snowflake) -> Option<&Cookie<'_>> { + let refresh_cookie_name = build_refresh_cookie_name(application_id); + jar.get(&refresh_cookie_name) +} + +fn build_refresh_cookie_name(application_id: Snowflake) -> String { + let application_id = crypto::totp::BASE32_NOPAD + .encode(application_id.to_string().as_bytes()) + .to_lowercase(); + let refresh_cookie_name = format!("refresh_{}", application_id); + + refresh_cookie_name +} diff --git a/services/authcore/src/grpc.rs b/services/authcore/src/grpc.rs index 700ce89..44f677d 100644 --- a/services/authcore/src/grpc.rs +++ b/services/authcore/src/grpc.rs @@ -5,7 +5,10 @@ use std::net::SocketAddr; use crate::AppState; +mod auth; +mod error; mod platform; +mod session; /// Tonic-generated gRPC bindings pub mod authcore { @@ -33,12 +36,20 @@ pub enum GrpcServerError { /// Currently, this function cannot return any errors as the implementation /// is a placeholder. pub async fn run(addr: SocketAddr, state: AppState) -> Result<(), GrpcServerError> { - let platform = platform::Platform::new(state); - let svc_platform = authcore::auth_core_platform_server::AuthCorePlatformServer::new(platform); + let inner = platform::PlatformServer::new(state.clone()); + let svc_platform = authcore::platform_server::PlatformServer::new(inner); + + let inner = auth::basic::BasicServer::new(state.clone()); + let svc_basic = auth::basic::basic_auth_server::BasicAuthServer::new(inner); + + let inner = session::SessionServer::new(state.clone()); + let svc_session = session::session_server::SessionServer::new(inner); tracing::info!("grpc listening on {}", addr); tonic::transport::Server::builder() .add_service(svc_platform) + .add_service(svc_basic) + .add_service(svc_session) .serve(addr) .await .map_err(GrpcServerError::GRPCServerError)?; diff --git a/services/authcore/src/grpc/auth.rs b/services/authcore/src/grpc/auth.rs new file mode 100644 index 0000000..38883ee --- /dev/null +++ b/services/authcore/src/grpc/auth.rs @@ -0,0 +1 @@ +pub mod basic; diff --git a/services/authcore/src/grpc/auth/basic.rs b/services/authcore/src/grpc/auth/basic.rs new file mode 100644 index 0000000..f392779 --- /dev/null +++ b/services/authcore/src/grpc/auth/basic.rs @@ -0,0 +1,107 @@ +use crypto::snowflake::Snowflake; +use tonic::Code; + +use crate::{ + core::basic::register::{self, BasicRegistrationData, BasicRegistrationError}, + grpc::error::DetailedError, + state::AppState, +}; + +/// Tonic-generated gRPC bindings +mod proto_basic { + tonic::include_proto!("authcore.auth.basic"); +} + +pub use proto_basic::*; + +use self::basic_auth_server::BasicAuth; + +pub struct BasicServer { + state: AppState, +} + +impl BasicServer { + pub fn new(state: AppState) -> Self { + Self { state } + } +} + +#[tonic::async_trait] +impl BasicAuth for BasicServer { + async fn register( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let data = request.into_inner(); + + // Convert the application ID to a snowflake + let application_id: Snowflake = if let Ok(id) = data.application_id.try_into() { + id + } else { + return Err(tonic::Status::invalid_argument("application id is invalid")); + }; + + // Configure the registration data + let data = BasicRegistrationData { + email: data.email, + + first_name: None, + last_name: None, + + password: data.password, + application_id, + }; + + fn parse_err(e: BasicRegistrationError) -> tonic::Status { + let error = match e { + BasicRegistrationError::EmailFormat => ( + ErrorCode::EmailFormat, + "invalid email format", + Code::InvalidArgument, + ), + BasicRegistrationError::PasswordFormat(_format) => { + // Todo: return format + ( + ErrorCode::PasswordFormat, + "invalid password format", + Code::InvalidArgument, + ) + } + BasicRegistrationError::AlreadyExists => ( + ErrorCode::AlreadyExists, + "user already exists", + Code::AlreadyExists, + ), + BasicRegistrationError::ApplicationDoesNotExist => ( + ErrorCode::ApplicationDoesNotExist, + "application does not exist", + Code::NotFound, + ), + BasicRegistrationError::InternalServerError => ( + ErrorCode::InternalServerError, + "internal server error", + Code::Internal, + ), + }; + + let register_error = DetailedError { + code: error.0.to_string(), + message: error.1.to_string(), + }; + + let out = serde_json::to_string(®ister_error).unwrap(); + + tonic::Status::new(error.2, out) + } + + // Try to register the user + let user = match register::with_basic_auth(&self.state, data).await { + Err(e) => return Err(parse_err(e)), + Ok(user) => user, + }; + + Ok(tonic::Response::new(RegisterResponse { + user_id: user.id().to_string(), + })) + } +} diff --git a/services/authcore/src/grpc/error.rs b/services/authcore/src/grpc/error.rs new file mode 100644 index 0000000..114031c --- /dev/null +++ b/services/authcore/src/grpc/error.rs @@ -0,0 +1,6 @@ +/// Tonic-generated gRPC bindings +pub mod tonic_error { + tonic::include_proto!("authcore.error"); +} + +pub use tonic_error::*; diff --git a/services/authcore/src/grpc/platform.rs b/services/authcore/src/grpc/platform.rs index df0432d..708b2b0 100644 --- a/services/authcore/src/grpc/platform.rs +++ b/services/authcore/src/grpc/platform.rs @@ -9,26 +9,26 @@ use crate::{ use super::authcore::{ AddApplicationRequest, AddApplicationResponse, DeleteApplicationRequest, - DeleteApplicationResponse, GetAuthCoreVersionRequest, GetAuthCoreVersionResponse, + DeleteApplicationResponse, GetVersionRequest, GetVersionResponse, }; -pub struct Platform { +pub struct PlatformServer { state: AppState, } -impl Platform { +impl PlatformServer { pub fn new(state: AppState) -> Self { Self { state } } } #[tonic::async_trait] -impl super::authcore::auth_core_platform_server::AuthCorePlatform for Platform { - async fn get_auth_core_version( +impl super::authcore::platform_server::Platform for PlatformServer { + async fn get_version( &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { - let response = GetAuthCoreVersionResponse { + _request: tonic::Request, + ) -> Result, tonic::Status> { + let response = GetVersionResponse { version: self.state.service_data().version.into(), }; diff --git a/services/authcore/src/grpc/session.rs b/services/authcore/src/grpc/session.rs new file mode 100644 index 0000000..0befef9 --- /dev/null +++ b/services/authcore/src/grpc/session.rs @@ -0,0 +1,46 @@ +/// Tonic-generated gRPC bindings +mod proto_session { + tonic::include_proto!("authcore.session"); +} + +pub use proto_session::*; +use tracing::error; + +use crate::{core, state::AppState}; + +use self::session_server::Session; + +pub struct SessionServer { + state: AppState, +} + +impl SessionServer { + pub fn new(state: AppState) -> Self { + Self { state } + } +} + +#[tonic::async_trait] +impl Session for SessionServer { + async fn validate( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let data = request.into_inner(); + + let result = core::token::verify_access_token(&self.state, &data.refresh_token); + if let Err(e) = result { + error!("Failed to validate access token: {:?}", e); + return Err(tonic::Status::unauthenticated("Invalid access token")); + } + + Ok(tonic::Response::new(ValidateResponse {})) + } + + async fn invalidate( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + unimplemented!() + } +} diff --git a/services/authcore/src/http/modules/basic.rs b/services/authcore/src/http/modules/basic.rs index 2646643..9409aa7 100644 --- a/services/authcore/src/http/modules/basic.rs +++ b/services/authcore/src/http/modules/basic.rs @@ -10,13 +10,9 @@ use crate::state::AppState; /// Login submodule for handling user authentication using a username/email and password. pub mod login; -/// Register submodule for handling user registration, including user creation and password storage. -pub mod register; - /// Router for handling routing within basic_auth. pub fn router(state: AppState) -> Router { Router::new() .route("/login", post(login::route)) - .route("/register", post(register::route)) .with_state(state) } diff --git a/services/authcore/src/http/modules/basic/login.rs b/services/authcore/src/http/modules/basic/login.rs index ace76c4..3ca87df 100644 --- a/services/authcore/src/http/modules/basic/login.rs +++ b/services/authcore/src/http/modules/basic/login.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use tracing::error; use crate::{ - core::basic::login, + core::{basic::login, token}, http::{modules::get_request, response::HTTPResponse}, state::AppState, }; @@ -21,8 +21,6 @@ pub struct LoginRequest { pub email: String, pub password: String, pub application_id: String, - - pub totp_code: Option, } #[derive(Serialize)] @@ -83,7 +81,6 @@ pub async fn route( data.password, application_id, addr.ip().to_string(), - data.totp_code, ) .await { @@ -171,9 +168,10 @@ pub async fn route( }; // Write refresh to cookie - let jar = jar.add(login::create_refresh_cookie( + let jar = jar.add(token::create_refresh_cookie( refresh_token.token().to_string(), refresh_token.expires_at(), + application_id, )); let response = HTTPResponse::ok(response); diff --git a/services/authcore/src/http/modules/basic/register.rs b/services/authcore/src/http/modules/basic/register.rs deleted file mode 100644 index 973ad8b..0000000 --- a/services/authcore/src/http/modules/basic/register.rs +++ /dev/null @@ -1,160 +0,0 @@ -use axum::{ - extract::{FromRequest, State}, - Form, Json, -}; -use crypto::snowflake::Snowflake; -use hyper::{Body, Request, StatusCode}; -use serde::Deserialize; - -use crate::{ - core::basic::register::{self, BasicRegistrationData}, - http::response::HTTPResponse, - state::AppState, -}; - -#[derive(Deserialize)] -pub struct RegisterRequest { - email: String, - password: String, - application_id: String, -} - -async fn get_register_request(request: Request) -> Option { - match request - .headers() - .get("content-type") - .and_then(|header| header.to_str().ok()) - { - Some("application/x-www-form-urlencoded") => { - let data = Form::::from_request(request, &()) - .await - .ok()?; - - Some(data.0) - } - Some("application/json") => { - let data = Json::::from_request(request, &()) - .await - .ok()?; - - Some(data.0) - } - _ => None, - } -} - -pub async fn route( - State(state): State, - request: Request, -) -> (StatusCode, Json) { - // Accept multiple different ways to give the data, url encoded form data or json body - let data: RegisterRequest = match get_register_request(request).await { - Some(d) => d, - None => { - let response = HTTPResponse::error( - "BadRequest", - "Invalid content type, expected application/x-www-form-urlencoded or application/json".to_owned(), - (), - ); - - return (StatusCode::BAD_REQUEST, Json(response)); - } - }; - - // Convert the application ID to a snowflake - let application_id: Snowflake = if let Ok(id) = data.application_id.try_into() { - id - } else { - let error = HTTPResponse::error( - "InvalidApplicationID", - "Invalid application ID".to_owned(), - (), - ); - - return (StatusCode::BAD_REQUEST, Json(error)); - }; - - // Configure the registration data - let data = BasicRegistrationData { - email: data.email, - - first_name: None, - last_name: None, - - password: data.password, - application_id, - }; - - // Try to register the user - if let Err(e) = register::with_basic_auth(&state, data).await { - tracing::error!("registration error: {:#?}", e); - - match e { - register::BasicRegistrationError::InvalidEmailAddressFormat => { - let error = HTTPResponse::error( - "InvalidEmailAddress", - "Invalid email address".to_owned(), - (), - ); - - return (StatusCode::BAD_REQUEST, Json(error)); - } - register::BasicRegistrationError::EmailAddressAlreadyExists => { - let error = HTTPResponse::error( - "AlreadyExists", - "Email address already in use".to_owned(), - (), - ); - - return (StatusCode::CONFLICT, Json(error)); - } - register::BasicRegistrationError::InvalidPassword(reason) => { - let error = - HTTPResponse::error("InvalidPassword", "Invalid password".to_owned(), reason); - - return (StatusCode::BAD_REQUEST, Json(error)); - } - register::BasicRegistrationError::InvalidUsernameFormat => { - let error = - HTTPResponse::error("InvalidUsername", "Invalid username".to_owned(), ()); - - return (StatusCode::BAD_REQUEST, Json(error)); - } - register::BasicRegistrationError::UsernameAlreadyExists => { - let error = - HTTPResponse::error("AlreadyExists", "Username already in use".to_owned(), ()); - - return (StatusCode::CONFLICT, Json(error)); - } - register::BasicRegistrationError::QueryError(_) => { - let error = HTTPResponse::error( - "InternalServerError", - "Internal server error".to_owned(), - (), - ); - - return (StatusCode::INTERNAL_SERVER_ERROR, Json(error)); - } - register::BasicRegistrationError::Unknown => { - let error = HTTPResponse::error( - "InternalServerError", - "Internal server error".to_owned(), - (), - ); - - return (StatusCode::INTERNAL_SERVER_ERROR, Json(error)); - } - register::BasicRegistrationError::ApplicationDoesNotExist => { - let error = HTTPResponse::error( - "ApplicationDoesNotExist", - "Application does not exist".to_owned(), - (), - ); - - return (StatusCode::BAD_REQUEST, Json(error)); - } - } - }; - - (StatusCode::OK, Json(HTTPResponse::empty())) -} diff --git a/services/authcore/src/http/modules/session/refresh.rs b/services/authcore/src/http/modules/session/refresh.rs index 3484d02..8629da0 100644 --- a/services/authcore/src/http/modules/session/refresh.rs +++ b/services/authcore/src/http/modules/session/refresh.rs @@ -5,10 +5,20 @@ use axum::{ Json, }; use axum_extra::extract::CookieJar; +use crypto::snowflake::Snowflake; use hyper::{Body, Request, StatusCode}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use crate::{core::token, http::response::HTTPResponse, state::AppState}; +use crate::{ + core::token::{self, get_refresh_cookie}, + http::{modules::get_request, response::HTTPResponse}, + state::AppState, +}; + +#[derive(Deserialize)] +pub struct RefreshRequest { + application_id: Snowflake, +} #[derive(Serialize)] pub struct RefreshResponse { @@ -19,10 +29,26 @@ pub async fn route( ConnectInfo(_addr): ConnectInfo, State(state): State, jar: CookieJar, - _request: Request, + request: Request, ) -> (StatusCode, Json) { + let (parts, body) = request.into_parts(); + + // Accept multiple different ways to give the data, url encoded form data or json body + let data: RefreshRequest = match get_request(&parts, body).await { + Some(d) => d, + None => { + let response = HTTPResponse::error( + "BadRequest", + "Invalid content type, expected application/x-www-form-urlencoded or application/json".to_owned(), + (), + ); + + return (StatusCode::BAD_REQUEST, Json(response)); + } + }; + // Get refresh token from cookie - let refresh = jar.get("refresh"); + let refresh = get_refresh_cookie(&jar, data.application_id); let token = match refresh { Some(refresh) => { // Fetch refresh token from database diff --git a/services/authcore/src/http/modules/totp/verify.rs b/services/authcore/src/http/modules/totp/verify.rs index 2588e27..e898e76 100644 --- a/services/authcore/src/http/modules/totp/verify.rs +++ b/services/authcore/src/http/modules/totp/verify.rs @@ -11,7 +11,7 @@ use hyper::{Body, Request, StatusCode}; use serde::Deserialize; use crate::{ - core::{basic::login, totp}, + core::{basic::login, token, totp}, http::{ modules::{basic::login::LoginResponse, get_request}, response::HTTPResponse, @@ -211,9 +211,10 @@ pub async fn route( }; // Write refresh to cookie - let jar = jar.add(login::create_refresh_cookie( + let jar = jar.add(token::create_refresh_cookie( refresh_token.token().to_string(), refresh_token.expires_at(), + user.application_id(), )); let response = HTTPResponse::ok(response); diff --git a/services/authcore/src/models/user/totp.rs b/services/authcore/src/models/user/totp.rs index 34508b5..a570e3d 100644 --- a/services/authcore/src/models/user/totp.rs +++ b/services/authcore/src/models/user/totp.rs @@ -1,6 +1,5 @@ use chrono::{DateTime, Utc}; use crypto::snowflake::Snowflake; -use tracing::info; use crate::models::{ error::ModelError, @@ -120,8 +119,6 @@ impl TOTP { impl From for TOTP { fn from(value: prisma::totp::Data) -> Self { - info!("TOTP: {:?}", value); - let totp_backup_codes = value .totp_backup_code .unwrap_or(Vec::new()) diff --git a/services/dashboard/next.config.js b/services/dashboard/next.config.js index 767719f..ca2914e 100644 --- a/services/dashboard/next.config.js +++ b/services/dashboard/next.config.js @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + serverActions: true, + }, +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/services/dashboard/package-lock.json b/services/dashboard/package-lock.json index 43f4547..b93c6bd 100644 --- a/services/dashboard/package-lock.json +++ b/services/dashboard/package-lock.json @@ -22,6 +22,9 @@ }, "../../SDK/auth-sdk-js": { "version": "0.1.0", + "dependencies": { + "next": "^13.4.19" + }, "devDependencies": { "@types/node": "^20.5.6", "@types/react": "^18.2.21", diff --git a/services/dashboard/src/app/layout.tsx b/services/dashboard/src/app/layout.tsx index f364f96..89f59bc 100644 --- a/services/dashboard/src/app/layout.tsx +++ b/services/dashboard/src/app/layout.tsx @@ -1,7 +1,7 @@ +import { AuthProvider } from "auth-sdk-js"; import "./globals.css"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; -import { AuthProvider } from 'auth-sdk-js'; const inter = Inter({ subsets: ["latin"] }); diff --git a/services/dashboard/src/app/page.module.css b/services/dashboard/src/app/page.module.css index e69de29..360014e 100644 --- a/services/dashboard/src/app/page.module.css +++ b/services/dashboard/src/app/page.module.css @@ -0,0 +1,19 @@ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem; + height: 4rem; + background-color: #fff; +} + +.headerName { + font-size: 1.5rem; + font-weight: 600; +} + +.headerLinks > nav { + display: flex; + align-items: center; + list-style: none; +} diff --git a/services/dashboard/src/app/page.tsx b/services/dashboard/src/app/page.tsx index 53443b3..35fcbd7 100644 --- a/services/dashboard/src/app/page.tsx +++ b/services/dashboard/src/app/page.tsx @@ -1,8 +1,23 @@ -import Image from "next/image"; import styles from "./page.module.css"; -export default function Home() { - return
- -
; +export default async function Home() { + return ( +
+
+
+

Auth

+
+
+ +
+
+
+ ); } diff --git a/services/platform/build.rs b/services/platform/build.rs index 66e02f2..078760e 100644 --- a/services/platform/build.rs +++ b/services/platform/build.rs @@ -1,4 +1,3 @@ fn main() -> Result<(), Box> { - tonic_build::compile_protos("../../proto/authcore.proto")?; Ok(()) }