diff --git a/example/.env b/example/.env.example similarity index 100% rename from example/.env rename to example/.env.example diff --git a/example/Cargo.toml b/example/Cargo.toml index e6ef0b6..f7cff7e 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -1,10 +1,8 @@ [package] -name = "actix-web-sample-app" +name = "actix-admin-example" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] actix-web = "4.0.1" actix-rt = "2.7.0" @@ -25,5 +23,6 @@ serde_derive = "1.0.136" quote = "1.0" sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } syn = "1.0.91" + actix_admin = { path = "../" } -azure_auth = { path = "../azure_auth" } +azure_auth = { path = "./azure_auth" } diff --git a/example/azure_auth/Cargo.toml b/example/azure_auth/Cargo.toml new file mode 100644 index 0000000..ce740b2 --- /dev/null +++ b/example/azure_auth/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "azure_auth" +version = "0.1.0" +edition = "2021" + +[dependencies] +actix-web = "4.0.1" +actix-rt = "2.7.0" +actix-session = "0.7.1" + +oauth2 = "4.1" +base64 = "0.13.0" +async-trait = "0.1.53" +rand = "0.8.5" +url = "2.2.2" +http = "0.2.6" +dotenv = "0.15" +futures = "0.3.21" +serde = "1.0.136" +serde_json = "1.0.79" +serde_derive = "1.0.136" \ No newline at end of file diff --git a/example/azure_auth/src/lib.rs b/example/azure_auth/src/lib.rs new file mode 100644 index 0000000..c8bf116 --- /dev/null +++ b/example/azure_auth/src/lib.rs @@ -0,0 +1,161 @@ +#[macro_use] +extern crate serde_derive; + +use actix_session::{Session}; +use actix_web::http::header; +use actix_web::{web, HttpResponse}; +use http::{HeaderMap, Method}; +use oauth2::basic::BasicClient; +use oauth2::reqwest::async_http_client; +use oauth2::{ + AccessToken, AuthorizationCode, CsrfToken, //PkceCodeChallenge, + Scope, TokenResponse +}; +use std::str; +use url::Url; +use oauth2::{ + AuthUrl, ClientId, ClientSecret, TokenUrl, +}; + +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug)] +pub struct UserInfo { + mail: String, + userPrincipalName: String, + displayName: String, + givenName: String, + surname: String, + id: String, + role: String +} + +// AppDataTrait +pub trait AppDataTrait { + fn get_oauth(&self) -> &BasicClient; +} + +#[derive(Clone, Debug)] +pub struct AzureAuth { + auth_url: AuthUrl, + token_url: TokenUrl, + client_id: ClientId, + client_secret: ClientSecret +} + +impl AzureAuth { + pub fn new(oauth2_server: &String, client_id: &String, client_secret: &String) -> Self { + let azure_auth = AzureAuth { + auth_url: AuthUrl::new(format!("https://{}/oauth2/v2.0/authorize", oauth2_server)).expect("Invalid authorization endpoint URL"), + token_url: TokenUrl::new(format!("https://{}/oauth2/v2.0/token", oauth2_server)).expect("Invalid token endpoint URL"), + client_id: ClientId::new(client_id.clone()), + client_secret: ClientSecret::new(client_secret.clone()) + }; + + azure_auth + } + + pub fn get_api_base_url() -> &'static str { + "https://graph.microsoft.com/v1.0" + } + + pub fn get_oauth_client(self) -> BasicClient { + BasicClient::new( + self.client_id, + Some(self.client_secret), + self.auth_url, + Some(self.token_url), + ) + } + + pub fn create_scope(self) -> actix_web::Scope { + let scope = web::scope("/auth") + .route("/login", web::get().to(login::)) + .route("/logout", web::get().to(logout)) + .route("/auth", web::get().to(auth::)) + ; + + scope + } +} + +pub async fn login(data: web::Data) -> HttpResponse { + // Create a PKCE code verifier and SHA-256 encode it as a code challenge. + // let (_pkce_code_challenge, _pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); + // Generate the authorization URL to which we'll redirect the user. + let (auth_url, _csrf_token) = &data + .get_oauth() + .authorize_url(CsrfToken::new_random) + // Set the desired scopes. + .add_scope(Scope::new("openid".to_string())) + // Set the PKCE code challenge, need to pass verifier to /auth. + //.set_pkce_challenge(pkce_code_challenge) + .url(); + + HttpResponse::Found() + .append_header((header::LOCATION, auth_url.to_string())) + .finish() +} + +pub async fn logout(session: Session) -> HttpResponse { + session.remove("user_info"); + HttpResponse::Found() + .append_header((header::LOCATION, "/".to_string())) + .finish() +} + +async fn read_user(api_base_url: &str, access_token: &AccessToken) -> UserInfo { + let url = Url::parse(format!("{}/me", api_base_url).as_str()).unwrap(); + + let mut headers = HeaderMap::new(); + headers.insert( + "Authorization", + format!("Bearer {}", access_token.secret()).parse().unwrap(), + ); + + let resp = async_http_client(oauth2::HttpRequest { + url, + method: Method::GET, + headers: headers, + body: Vec::new(), + }) + .await + .expect("Request failed"); + + let s: &str = match str::from_utf8(&resp.body) { + Ok(v) => v, + Err(e) => panic!("Invalid UTF-8 sequence: {}", e), + }; + + serde_json::from_slice(&resp.body).unwrap() +} + +#[derive(Deserialize)] +pub struct AuthRequest { + code: String, + state: String, +} + +pub async fn auth( + session: Session, + data: web::Data, + params: web::Query, +) -> HttpResponse { + let code = AuthorizationCode::new(params.code.clone()); + let _state = CsrfToken::new(params.state.clone()); + let api_base_url = AzureAuth::get_api_base_url(); + + // Exchange the code with a token. + let token = &data + .get_oauth() + .exchange_code(code) + //.set_pkce_verifier() + .request_async(async_http_client) + .await + .expect("exchange_code failed"); + + let user_info = read_user(api_base_url, token.access_token()).await; + + session.insert("user_info", &user_info).unwrap(); + + HttpResponse::Found().append_header(("location", "/")).finish() +}