auth0 example

This commit is contained in:
Cadey Ratio 2020-04-15 22:08:05 -04:00
commit 6022dd7cf7
17 changed files with 2411 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
eval "$(lorri direnv)"

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.env

2064
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "xatci"
version = "0.1.0"
authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018"
[dependencies]
actix-rt = "1"
actix-service = "1"
actix-web = "2"
actix-web-httpauth = "0.4"
alcoholic_jwt = "1"
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
derive_more = "0"
diesel = { version = "1", features = ["postgres","uuidv07", "r2d2", "chrono"] }
futures = "0.3"
kankyo = "0.3"
log = "0.4"
pretty_env_logger = "0"
r2d2 = "0.8"
reqwest = { version = "0.10", features = ["json"] }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
thiserror = "1.0"

5
diesel.toml Normal file
View File

@ -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"

0
migrations/.gitkeep Normal file
View File

View File

@ -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();

View File

@ -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;

View File

@ -0,0 +1 @@
DROP TABLE users;

View File

@ -0,0 +1,7 @@
CREATE TABLE users (
id SERIAL NOT NULL PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);

17
shell.nix Normal file
View File

@ -0,0 +1,17 @@
let pkgs = import <nixpkgs> { };
in pkgs.mkShell {
buildInputs = with pkgs; [
rustc
cargo
rls
rustfmt
openssl
pkg-config
postgresql
diesel-cli
];
DATABASE_URL = "postgres://xatci:hunter2@127.0.0.1/xatci?sslmode=disable";
DOMAIN = "https://xeserv.auth0.com/";
}

30
src/auth.rs Normal file
View File

@ -0,0 +1,30 @@
use crate::errors::ServiceError;
use alcoholic_jwt::{token_kid, validate, Validation, JWKS};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
company: String,
exp: usize,
}
pub async fn validate_token(token: &str) -> Result<bool, ServiceError> {
let domain = std::env::var("DOMAIN").expect("DOMAIN must be set");
let jwks = fetch_jwks(&format!("{}{}", domain.as_str(), ".well-known/jwks.json")).await
.expect("failed to fetch jwks");
let validations = vec![Validation::Issuer(domain), Validation::SubjectPresent];
let kid = match token_kid(&token) {
Ok(res) => res.expect("failed to decode kid"),
Err(_) => return Err(ServiceError::JWKSFetchError),
};
let jwk = jwks.find(&kid).expect("Specified key not found in set");
let res = validate(token, jwk, validations);
Ok(res.is_ok())
}
async fn fetch_jwks(uri: &str) -> anyhow::Result<JWKS> {
let res = reqwest::get(uri).await?;
let val = res.json::<JWKS>().await?;
return Ok(val);
}

29
src/errors.rs Normal file
View File

@ -0,0 +1,29 @@
use actix_web::{error::ResponseError, HttpResponse};
use derive_more::Display;
#[derive(Debug, Display)]
pub enum ServiceError {
#[display(fmt = "Internal Server Error")]
InternalServerError,
#[display(fmt = "BadRequest: {}", _0)]
BadRequest(String),
#[display(fmt = "JWKSFetchError")]
JWKSFetchError,
}
// impl ResponseError trait allows to convert our errors into http responses with appropriate data
impl ResponseError for ServiceError {
fn error_response(&self) -> HttpResponse {
match self {
ServiceError::InternalServerError => {
HttpResponse::InternalServerError().json("Internal Server Error, Please try later")
}
ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
ServiceError::JWKSFetchError => {
HttpResponse::InternalServerError().json("Could not fetch JWKS")
}
}
}
}

92
src/handlers.rs Normal file
View File

@ -0,0 +1,92 @@
use super::models::{NewUser, User};
use super::schema::users::dsl::*;
use super::Pool;
use crate::diesel::QueryDsl;
use crate::diesel::RunQueryDsl;
use actix_web::{web, Error, HttpResponse, Responder};
use diesel::dsl::{delete, insert_into};
use serde::{Deserialize, Serialize};
use std::vec::Vec;
#[derive(Debug, Serialize, Deserialize)]
pub struct InputUser {
pub first_name: String,
pub last_name: String,
pub email: String,
}
pub async fn get_users(db: web::Data<Pool>) -> Result<HttpResponse, Error> {
Ok(web::block(move || get_all_users(db))
.await
.map(|user| HttpResponse::Ok().json(user))
.map_err(|_| HttpResponse::InternalServerError())?)
}
fn get_all_users(pool: web::Data<Pool>) -> Result<Vec<User>, diesel::result::Error> {
let conn = pool.get().unwrap();
let items = users.load::<User>(&conn)?;
Ok(items)
}
// Handler for GET /users/{id}
pub async fn get_user_by_id(
db: web::Data<Pool>,
user_id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
Ok(
web::block(move || db_get_user_by_id(db, user_id.into_inner()))
.await
.map(|user| HttpResponse::Ok().json(user))
.map_err(|_| HttpResponse::InternalServerError())?,
)
}
// Handler for POST /users
pub async fn add_user(
db: web::Data<Pool>,
item: web::Json<InputUser>,
) -> Result<HttpResponse, Error> {
Ok(web::block(move || add_single_user(db, item))
.await
.map(|user| HttpResponse::Created().json(user))
.map_err(|_| HttpResponse::InternalServerError())?)
}
// Handler for DELETE /users/{id}
pub async fn delete_user(
db: web::Data<Pool>,
user_id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
Ok(
web::block(move || delete_single_user(db, user_id.into_inner()))
.await
.map(|user| HttpResponse::Ok().json(user))
.map_err(|_| HttpResponse::InternalServerError())?,
)
}
fn db_get_user_by_id(pool: web::Data<Pool>, user_id: i32) -> Result<User, diesel::result::Error> {
let conn = pool.get().unwrap();
users.find(user_id).get_result::<User>(&conn)
}
fn add_single_user(
db: web::Data<Pool>,
item: web::Json<InputUser>,
) -> Result<User, diesel::result::Error> {
let conn = db.get().unwrap();
let new_user = NewUser {
first_name: &item.first_name,
last_name: &item.last_name,
email: &item.email,
created_at: chrono::Local::now().naive_local(),
};
let res = insert_into(users).values(&new_user).get_result(&conn)?;
Ok(res)
}
fn delete_single_user(db: web::Data<Pool>, user_id: i32) -> Result<usize, diesel::result::Error> {
let conn = db.get().unwrap();
let count = delete(users.find(user_id)).execute(&conn)?;
Ok(count)
}

66
src/main.rs Normal file
View File

@ -0,0 +1,66 @@
#[macro_use]
extern crate diesel;
use actix_web::{dev::ServiceRequest, web, App, Error, HttpServer};
use actix_web_httpauth::{
extractors::{
bearer::{BearerAuth, Config},
AuthenticationError,
},
middleware::HttpAuthentication,
};
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
mod auth;
mod errors;
mod handlers;
mod models;
mod schema;
pub type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let _ = kankyo::init();
pretty_env_logger::init();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// create db connection pool
let manager = ConnectionManager::<PgConnection>::new(database_url);
let pool: Pool = r2d2::Pool::builder()
.build(manager)
.expect("Failed to create pool.");
// Start http server
HttpServer::new(move || {
let auth = HttpAuthentication::bearer(validator);
App::new()
.wrap(auth)
.data(pool.clone())
.route("/users", web::get().to(handlers::get_users))
.route("/users/{id}", web::get().to(handlers::get_user_by_id))
.route("/users", web::post().to(handlers::add_user))
.route("/users/{id}", web::delete().to(handlers::delete_user))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, Error> {
let config = req
.app_data::<Config>()
.map(|data| data.get_ref().clone())
.unwrap_or_else(Default::default);
match auth::validate_token(credentials.token()).await {
Ok(res) => {
if res == true {
Ok(req)
} else {
Err(AuthenticationError::from(config).into())
}
}
Err(_) => Err(AuthenticationError::from(config).into()),
}
}

21
src/models.rs Normal file
View File

@ -0,0 +1,21 @@
use crate::schema::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Queryable)]
pub struct User {
pub id: i32,
pub first_name: String,
pub last_name: String,
pub email: String,
pub created_at: chrono::NaiveDateTime,
}
#[derive(Insertable, Debug)]
#[table_name = "users"]
pub struct NewUser<'a> {
pub first_name: &'a str,
pub last_name: &'a str,
pub email: &'a str,
pub created_at: chrono::NaiveDateTime,
}

9
src/schema.rs Normal file
View File

@ -0,0 +1,9 @@
table! {
users (id) {
id -> Int4,
first_name -> Text,
last_name -> Text,
email -> Text,
created_at -> Timestamp,
}
}