Skip to content

Commit

Permalink
work on auth. moving REST /auth to /graphql, still need to setup csrf…
Browse files Browse the repository at this point in the history
… for new endpoint
  • Loading branch information
kmurf1999 committed Dec 21, 2020
1 parent b094b72 commit e944478
Show file tree
Hide file tree
Showing 24 changed files with 536 additions and 135 deletions.
6 changes: 4 additions & 2 deletions api/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ pub struct Request {

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Claims {
session: String,
csrf: String,
// TODO maybe dont make this public, but it shouldnt matter too much
pub session: String,
pub csrf: String,
}

impl Claims {
Expand Down Expand Up @@ -63,6 +64,7 @@ pub async fn filter(
Ok(reply)
}

// login route
async fn request(
env: Environment,
req: Request,
Expand Down
2 changes: 1 addition & 1 deletion api/src/environment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub struct Environment {
redis: redis::Client,
argon: Argon,
jwt: Jwt,
session_lifetime: Option<i64>,
session_lifetime: Option<i64>
}

impl Environment {
Expand Down
2 changes: 1 addition & 1 deletion api/src/graphql/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ impl Context {
pub fn session(&self) -> Option<&Session> {
self.session.as_ref()
}

pub fn is_authenticated(&self) -> bool {
self.session.is_some()
}
Expand Down
9 changes: 1 addition & 8 deletions api/src/graphql/mutation/account.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
use crate::graphql::{mutation::Mutation, Context};
use crate::graphql::Context;
use crate::{auth, model};
use juniper::FieldResult;
use uuid::Uuid;

#[juniper::graphql_object(Context = Context)]
impl Mutation {
fn account() -> AccountMutation {
AccountMutation
}
}

#[derive(juniper::GraphQLInputObject, Debug)]
pub struct CreateAccountInput {
email: String,
Expand Down
137 changes: 137 additions & 0 deletions api/src/graphql/mutation/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use crate::graphql::Context;
use crate::{auth, model};
use rand::{distributions::Alphanumeric, Rng};
use thiserror::Error;
use juniper::FieldResult;
use chrono::{Duration, Utc};
// use std::net::SocketAddr;
use uuid::Uuid;

#[derive(juniper::GraphQLInputObject, Debug)]
pub struct AuthInput {
email: String,
password: String,
}

#[allow(dead_code)]
#[derive(Error, Debug)]
pub enum AuthError {
#[error("invalid credentials")]
InvalidCredentials,
#[error("could not hash password")]
ArgonError,
}


pub struct AuthMutation;

#[juniper::graphql_object(Context = Context)]
impl AuthMutation {
async fn register(ctx: &Context, input: AuthInput) -> FieldResult<model::Auth> {
let argon = ctx.argon();

let AuthInput { email, password } = input;
let password = argon.hasher().with_password(password).hash()?;
let id = Uuid::new_v4();

crate::sql::account::create_account(ctx.database(), id, &email, &password).await?;

let account = crate::sql::account::get_account(ctx.database(), &email).await?;

let identity = model::session::Identity {
fingerprint: None,
// TODO actually get remote IP
ip: None
};

let claims = auth::Claims {
session: rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(64)
.collect(),
csrf: rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(64)
.collect(),
};

let csrf = claims.csrf.clone();
// TODO make request lifetime a custom field
let expiry = Utc::now() + Duration::seconds(ctx.session_lifetime(Some(1000000)));

crate::sql::account::create_session(
ctx.database(),
&claims.session,
&claims.csrf,
account.id,
identity,
expiry,
)
.await?;

let jwt = ctx.jwt().encode(claims, expiry)?;

Ok(model::Auth {
jwt,
csrf
})
}
async fn login(ctx: &Context, input: AuthInput) -> FieldResult<model::Auth> {
let AuthInput { email, password } = input;

let account = crate::sql::account::get_account_id_password_by_email(ctx.database(), &email)
.await?
.ok_or(AuthError::InvalidCredentials)?;

let is_valid = ctx
.argon()
.verifier()
.with_hash(&account.password)
.with_password(&password)
.verify()
.or(Err(AuthError::ArgonError))?;

if !is_valid {
return Err(AuthError::InvalidCredentials.into());
}

let identity = model::session::Identity {
fingerprint: None,
// TODO actually get remote IP
ip: None
};

let claims = auth::Claims {
session: rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(64)
.collect(),
csrf: rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(64)
.collect(),
};

let csrf = claims.csrf.clone();
// TODO make request lifetime a custom field
let expiry = Utc::now() + Duration::seconds(ctx.session_lifetime(Some(1000000)));

crate::sql::account::create_session(
ctx.database(),
&claims.session,
&claims.csrf,
account.id,
identity,
expiry,
)
.await?;

let jwt = ctx.jwt().encode(claims, expiry)?;

Ok(model::Auth {
jwt,
csrf
})
}
}

18 changes: 18 additions & 0 deletions api/src/graphql/mutation/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
use crate::graphql::Context;

mod account;
mod auth;

use account::AccountMutation;
use auth::AuthMutation;

pub struct Mutation;

#[juniper::graphql_object(Context = Context)]
impl Mutation {
fn auth() -> AuthMutation {
AuthMutation
}
fn account() -> AccountMutation {
AccountMutation
}
}


8 changes: 8 additions & 0 deletions api/src/model/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use juniper::GraphQLObject;
use serde::{Deserialize, Serialize};

#[derive(Clone, Serialize, Deserialize, GraphQLObject, Debug)]
pub struct Auth {
pub csrf: String,
pub jwt: String
}
2 changes: 2 additions & 0 deletions api/src/model/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub mod account;
mod redacted;
pub mod session;
pub mod auth;

pub use account::Account;
pub use session::Session;
pub use auth::Auth;
17 changes: 3 additions & 14 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ services:
volumes:
- ./frontend:/app
- "/app/node_modules"
env_file:
- ./.env.dev
ports:
- 3000:3000
stdin_open: true

api:
api: # rust graphql server
build:
context: ./api
dockerfile: Dockerfile.dev
Expand All @@ -22,9 +24,6 @@ services:
environment:
RUST_LOG: info
DATABASE_URL: postgres://postgres:postgres@db:5432/postgres
# REDIS_URL: redis://cache:6379/
# JWT_SECRET: ITS A SECRET
# ARGON_SECRET: ITS ANOTHER SECRET
ports:
- 3535:3535
depends_on:
Expand All @@ -41,20 +40,10 @@ services:
- ./migrations/up:/docker-entrypoint-initdb.d/
env_file:
- ./.env.dev
# environment:
# - POSTGRES_DB=postgres
# - POSTGRES_USER=postgres
# - POSTGRES_PASSWORD=postgres
restart: always

cache:
image: redis:latest
ports:
- 6379:6379
restart: always

adminer:
image: adminer
restart: always
ports:
- 8080:8080
File renamed without changes.
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.3.6",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
Expand All @@ -15,6 +16,7 @@
"@types/react-router-dom": "^5.1.6",
"@types/styled-components": "^5.1.2",
"@types/throttle-debounce": "^2.1.0",
"graphql": "^15.4.0",
"react": "^16.13.1",
"react-addons-update": "^15.6.3",
"react-dom": "^16.13.1",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/apollo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
uri: 'http://localhost:3535/graphql/query',
cache: new InMemoryCache()
});

export default client;
24 changes: 16 additions & 8 deletions frontend/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import React, { ReactElement, MouseEvent } from 'react';
import styled from 'styled-components';
import { colors } from '../styles';
import { shadow, colors } from '../styles';

type ButtonProps = {
variant?: 'default' | 'primary' | 'warning';
className?: string;
block?: boolean;
icon?: ReactElement;
children: ReactElement;
children: ReactElement | string;
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
};

const ButtonStyle = styled.button<{ variant: string }>`
const ButtonStyle = styled.button<{ block: boolean, variant: string }>`
text-align: center;
font-size: 0.9em;
font-size: 1em;
font-family: 'Roboto', 'sans-serif';
outline: none;
border: none;
cursor: pointer;
padding: 0.8em 1em;
padding: .8em 1.2em;
box-shadow: ${shadow[0]};
border-radius: 2px;
width: ${props => props.block ? '100%': 'fit-content'};
transition: transform .1s ease;
background: ${(props) => {
switch (props.variant) {
case 'primary':
return colors.primary;
case 'warning':
return colors.warning;
default:
return '#fff';
return '#eee';
}
}};
color: ${(props) => {
Expand All @@ -38,15 +42,19 @@ const ButtonStyle = styled.button<{ variant: string }>`
return 'rgba(0,0,0,0.65)';
}
}};
&:hover {
transform: scale(1.01);
box-shadow: ${shadow[1]};
}
> svg {
margin-left: 0.8em;
}
`;

function Button(props: ButtonProps): React.ReactElement {
const { variant = 'default', children, className = '', icon = null, onClick } = props;
const { block = false, variant = 'default', children, className = '', icon = null, onClick } = props;
return (
<ButtonStyle onClick={onClick} className={className} variant={variant}>
<ButtonStyle block={block} onClick={onClick} className={className} variant={variant}>
{children}
{icon}
</ButtonStyle>
Expand Down
Loading

0 comments on commit e944478

Please sign in to comment.