Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simple-auth-server added to examples #65

Merged
merged 5 commits into from
Dec 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# intellij files
.idea/**
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ script:
cd protobuf && cargo check && cd ..
cd r2d2 && cargo check && cd ..
cd redis-session && cargo check && cd ..
cd simple-auth-sarver && cargo check && cd ..
cd state && cargo check && cd ..
cd static_index && cargo check && cd ..
cd template_askama && cargo check && cd ..
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ members = [
"protobuf",
"r2d2",
"redis-session",
"simple-auth-server",
"state",
"static_index",
"template_askama",
Expand Down
22 changes: 22 additions & 0 deletions simple-auth-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "simple-auth-server"
version = "0.1.0"
authors = ["mygnu <tech@hgill.io>"]

[dependencies]
actix = "0.7.7"
actix-web = "0.7.14"
bcrypt = "0.2.1"
chrono = { version = "0.4.6", features = ["serde"] }
diesel = { version = "1.3.3", features = ["postgres", "uuid", "r2d2", "chrono"] }
dotenv = "0.13.0"
env_logger = "0.6.0"
failure = "0.1.3"
jsonwebtoken = "5.0"
futures = "0.1"
r2d2 = "0.8.3"
serde_derive="1.0.80"
serde_json="1.0"
serde="1.0"
sparkpost = "0.5.2"
uuid = { version = "0.6.5", features = ["serde", "v4"] }
32 changes: 32 additions & 0 deletions simple-auth-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
##### Flow of the event would look like this:

- Registers with email address ➡ Receive an 📨 with a link to verify
- Follow the link ➡ register with same email and a password
- Login with email and password ➡ Get verified and receive jwt token

##### Crates we are going to use

- [actix](https://crates.io/crates/actix) // Actix is a Rust actors framework.
- [actix-web](https://crates.io/crates/actix-web) // Actix web is a simple, pragmatic and extremely fast web framework for Rust.
- [brcypt](https://crates.io/crates/bcrypt) // Easily hash and verify passwords using bcrypt.
- [chrono](https://crates.io/crates/chrono) // Date and time library for Rust.
- [diesel](https://crates.io/crates/diesel) // A safe, extensible ORM and Query Builder for PostgreSQL, SQLite, and MySQL.
- [dotenv](https://crates.io/crates/dotenv) // A dotenv implementation for Rust.
- [env_logger](https://crates.io/crates/env_logger) // A logging implementation for log which is configured via an environment variable.
- [failure](https://crates.io/crates/failure) // Experimental error handling abstraction.
- [jsonwebtoken](https://crates.io/crates/jsonwebtoken) // Create and parse JWT in a strongly typed way.
- [futures](https://crates.io/crates/futures) // An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces.
- [r2d2](https://crates.io/crates/r2d2) // A generic connection pool.
- [serde](https://crates.io/crates/serde) // A generic serialization/deserialization framework.
- [serde_json](https://crates.io/crates/serde_json) // A JSON serialization file format.
- [serde_derive](https://crates.io/crates/serde_derive) // Macros 1.1 implementation of #[derive(Serialize, Deserialize)].
- [sparkpost](https://crates.io/crates/sparkpost) // Rust bindings for sparkpost email api v1.
- [uuid](https://crates.io/crates/uuid) // A library to generate and parse UUIDs.


Read the full tutorial series on [hgill.io](https://hgill.io)

- [Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 1](https://hgill.io/posts/auth-microservice-rust-actix-web-diesel-complete-tutorial-part-1/)
- [Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 2](https://hgill.io/posts/auth-microservice-rust-actix-web-diesel-complete-tutorial-part-2/)
- [Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 3](https://hgill.io/posts/auth-microservice-rust-actix-web-diesel-complete-tutorial-part-3/)

5 changes: 5 additions & 0 deletions simple-auth-server/diesel.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli

[print_schema]
file = "src/schema.rs"
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.

DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.




-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE users;
6 changes: 6 additions & 0 deletions simple-auth-server/migrations/2018-10-09-101948_users/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Your SQL goes here
CREATE TABLE users (
email VARCHAR(100) NOT NULL UNIQUE PRIMARY KEY,
password VARCHAR(64) NOT NULL, --bcrypt hash
created_at TIMESTAMP NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE invitations;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Your SQL goes here
CREATE TABLE invitations (
id UUID NOT NULL UNIQUE PRIMARY KEY,
email VARCHAR(100) NOT NULL,
expires_at TIMESTAMP NOT NULL
);
54 changes: 54 additions & 0 deletions simple-auth-server/src/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use actix::prelude::*;
use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{fs, http::Method, middleware::Logger, App};
use auth_routes::{get_me, login, logout};
use chrono::Duration;
use invitation_routes::register_email;
use models::DbExecutor;
use register_routes::register_user;

pub struct AppState {
pub db: Addr<DbExecutor>,
}

/// creates and returns the app after mounting all routes/resources
pub fn create_app(db: Addr<DbExecutor>) -> App<AppState> {
// secret is a random minimum 32 bytes long base 64 string
let secret: String = std::env::var("SECRET_KEY").unwrap_or_else(|_| "0123".repeat(8));
let domain: String = std::env::var("DOMAIN").unwrap_or_else(|_| "localhost".to_string());

App::with_state(AppState { db })
.middleware(Logger::default())
.middleware(IdentityService::new(
CookieIdentityPolicy::new(secret.as_bytes())
.name("auth")
.path("/")
.domain(domain.as_str())
.max_age(Duration::days(1))
.secure(false), // this can only be true if you have https
))
// everything under '/api/' route
.scope("/api", |api| {
// routes for authentication
api.resource("/auth", |r| {
r.method(Method::POST).with(login);
r.method(Method::DELETE).with(logout);
r.method(Method::GET).with(get_me);
})
// routes to invitation
.resource("/invitation", |r| {
r.method(Method::POST).with(register_email);
})
// routes to register as a user after the
.resource("/register/{invitation_id}", |r| {
r.method(Method::POST).with(register_user);
})
})
// serve static files
.handler(
"/",
fs::StaticFiles::new("./static/")
.unwrap()
.index_file("index.html"),
)
}
55 changes: 55 additions & 0 deletions simple-auth-server/src/auth_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use actix::{Handler, Message};
use diesel::prelude::*;
use errors::ServiceError;
use models::{DbExecutor, User, SlimUser};
use bcrypt::verify;
use actix_web::{FromRequest, HttpRequest, middleware::identity::RequestIdentity};
use utils::decode_token;

#[derive(Debug, Deserialize)]
pub struct AuthData {
pub email: String,
pub password: String,
}

impl Message for AuthData {
type Result = Result<SlimUser, ServiceError>;
}

mygnu marked this conversation as resolved.
Show resolved Hide resolved
impl Handler<AuthData> for DbExecutor {
type Result = Result<SlimUser, ServiceError>;
fn handle(&mut self, msg: AuthData, _: &mut Self::Context) -> Self::Result {
use schema::users::dsl::{users, email};
let conn: &PgConnection = &self.0.get().unwrap();

let mut items = users
.filter(email.eq(&msg.email))
.load::<User>(conn)?;

if let Some(user) = items.pop() {
match verify(&msg.password, &user.password) {
Ok(matching) => if matching {
return Ok(user.into());
},
Err(_) => (),
}
}
Err(ServiceError::BadRequest("Username and Password don't match".into()))
}
}

// we need the same data
// simple aliasing makes the intentions clear and its more readable
pub type LoggedUser = SlimUser;

impl<S> FromRequest<S> for LoggedUser {
type Config = ();
type Result = Result<LoggedUser, ServiceError>;
fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result {
if let Some(identity) = req.identity() {
let user: SlimUser = decode_token(&identity)?;
return Ok(user as LoggedUser);
}
Err(ServiceError::Unauthorized)
}
}
32 changes: 32 additions & 0 deletions simple-auth-server/src/auth_routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use actix_web::{AsyncResponder, FutureResponse, HttpResponse, HttpRequest, ResponseError, Json};
use actix_web::middleware::identity::RequestIdentity;
use futures::future::Future;
use utils::create_token;

use app::AppState;
use auth_handler::{AuthData, LoggedUser};

pub fn login((auth_data, req): (Json<AuthData>, HttpRequest<AppState>))
DoumanAsh marked this conversation as resolved.
Show resolved Hide resolved
-> FutureResponse<HttpResponse> {
req.state()
.db
.send(auth_data.into_inner())
.from_err()
.and_then(move |res| match res {
Ok(user) => {
let token = create_token(&user)?;
req.remember(token);
Ok(HttpResponse::Ok().into())
}
Err(err) => Ok(err.error_response()),
}).responder()
}

pub fn logout(req: HttpRequest<AppState>) -> HttpResponse {
req.forget();
HttpResponse::Ok().into()
}

pub fn get_me(logged_user: LoggedUser) -> HttpResponse {
HttpResponse::Ok().json(logged_user)
}
68 changes: 68 additions & 0 deletions simple-auth-server/src/email_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use models::Invitation;
use sparkpost::transmission::{
EmailAddress, Message, Options, Recipient, Transmission, TransmissionResponse,
};

fn get_api_key() -> String {
std::env::var("SPARKPOST_API_KEY").expect("SPARKPOST_API_KEY must be set")
}

pub fn send_invitation(invitation: &Invitation) {
let tm = Transmission::new_eu(get_api_key());
let sending_email =
std::env::var("SENDING_EMAIL_ADDRESS").expect("SENDING_EMAIL_ADDRESS must be set");
// new email message with sender name and email
let mut email = Message::new(EmailAddress::new(sending_email, "Let's Organise"));

let options = Options {
open_tracking: false,
click_tracking: false,
transactional: true,
sandbox: false,
inline_css: false,
start_time: None,
};

// recipient from the invitation email
let recipient: Recipient = invitation.email.as_str().into();

let email_body = format!(
"Please click on the link below to complete registration. <br/>
<a href=\"http://localhost:3000/register.html?id={}&email={}\">
http://localhost:3030/register</a> <br>
your Invitation expires on <strong>{}</strong>",
invitation.id,
invitation.email,
invitation
.expires_at
.format("%I:%M %p %A, %-d %B, %C%y")
.to_string()
);


// complete the email message with details
email
.add_recipient(recipient)
.options(options)
.subject("You have been invited to join Simple-Auth-Server Rust")
.html(email_body);

let result = tm.send(&email);

// Note that we only print out the error response from email api
match result {
Ok(res) => {
match res {
TransmissionResponse::ApiResponse(api_res) => {
println!("API Response: \n {:#?}", api_res);
}
TransmissionResponse::ApiError(errors) => {
println!("Response Errors: \n {:#?}", &errors);
}
}
}
Err(error) => {
println!("error \n {:#?}", error);
}
}
}
Loading