You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
232 lines
8.0 KiB
232 lines
8.0 KiB
use http::Host;
|
|
use rocket::Route;
|
|
use rocket::State;
|
|
use rocket::request::Form;
|
|
use rocket::response::Redirect;
|
|
use rocket_contrib::Json;
|
|
|
|
use oidc::{Token, UserInfo, Configuration, JSONWebKey, JSONWebKeySet};
|
|
use http::BearerToken;
|
|
|
|
use std::sync::Mutex;
|
|
use std::time::Duration;
|
|
use std::default::Default;
|
|
|
|
use config::Config;
|
|
use model::token;
|
|
use model::token::TokenStore;
|
|
|
|
use model::session::Session;
|
|
|
|
use model::key::Key;
|
|
|
|
use uuid::Uuid;
|
|
use url::Url;
|
|
use jwt::{self, Header, Registered};
|
|
use crypto::sha2::Sha256;
|
|
|
|
use time::get_time;
|
|
|
|
use frank_jwt;
|
|
|
|
static AUTH_CODE_TYPE: &'static str = "openid authentication code";
|
|
static ACCESS_TOKEN_TYPE: &'static str = "openid connect access token";
|
|
static REFRESH_TOKEN_TYPE: &'static str = "openid connect refresh token";
|
|
|
|
// http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
|
#[derive(Debug, FromForm)]
|
|
struct AuthenticationRequest {
|
|
scope: String,
|
|
response_type: String,
|
|
client_id: String,
|
|
redirect_uri: String,
|
|
state: Option<String>,
|
|
response_mode: Option<String>,
|
|
nonce: Option<String>,
|
|
display: Option<String>,
|
|
prompt: Option<String>,
|
|
max_age: Option<u32>,
|
|
ui_locales: Option<String>,
|
|
id_token_hint: Option<String>,
|
|
login_hint: Option<String>,
|
|
acr_values: Option<String>
|
|
}
|
|
|
|
// http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
|
#[post("/oidc/authenticate", data="<form>")]
|
|
fn authenticate_post(token_store_mutex: State<Mutex<Box<TokenStore>>>, session: Option<Session>, form: Form<AuthenticationRequest>) -> Option<Redirect> {
|
|
match session {
|
|
Some(session) => {
|
|
let mut token_store = token_store_mutex.lock().expect("Failed to acquire lock on token store");
|
|
authenticate(&mut token_store, session, form.get())
|
|
},
|
|
None => Some(Redirect::to("/login"))
|
|
}
|
|
}
|
|
|
|
#[get("/oidc/authenticate?<request>")]
|
|
fn authenticate_get(token_store_mutex: State<Mutex<Box<TokenStore>>>, session: Option<Session>, request: AuthenticationRequest) -> Option<Redirect> {
|
|
println!("{:?}", request);
|
|
match session {
|
|
Some(session) => {
|
|
let mut token_store = token_store_mutex.lock().expect("Failed to acquire lock on token store");
|
|
authenticate(&mut token_store, session, &request)
|
|
},
|
|
None => Some(Redirect::to("/login"))
|
|
}
|
|
}
|
|
|
|
fn authenticate(token_store: &mut Box<TokenStore>, session: Session, request: &AuthenticationRequest) -> Option<Redirect> {
|
|
Url::parse(&request.redirect_uri).ok().as_mut().map(|parsed_url| {
|
|
let code = Uuid::new_v4().hyphenated().to_string();
|
|
let mut query_params: Vec<String> = parsed_url.query().map(|query| {
|
|
query.split("&").map(|item| item.to_owned()).collect()
|
|
}).unwrap_or(vec![]);
|
|
token_store.put_token(token::Token {
|
|
token: code.clone(),
|
|
token_type: AUTH_CODE_TYPE.to_owned(),
|
|
subject: session.get_subject().expect("No valid subject").to_owned(),
|
|
expiry: None
|
|
});
|
|
|
|
query_params.push(format!("code={}", code));
|
|
if let Some(ref state) = request.state {
|
|
query_params.push(format!("state={}", state));
|
|
}
|
|
parsed_url.set_query(Some(&query_params.join("&")));
|
|
Redirect::to(&parsed_url.to_string())
|
|
})
|
|
}
|
|
|
|
// http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
|
#[derive(Debug, FromForm)]
|
|
struct TokenRequest {
|
|
grant_type: String,
|
|
code: String,
|
|
redirect_uri: String,
|
|
client_id: Option<String>,
|
|
scope: Option<String>
|
|
}
|
|
|
|
// http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
|
#[post("/oidc/token", data="<form>")]
|
|
fn token(key: State<Key>, config: State<Config>, token_store_mutex: State<Mutex<Box<TokenStore>>>, form: Form<TokenRequest>, host: Option<Host>) -> Option<Json<Token>> {
|
|
println!("{:?}", form.get());
|
|
let mut token_store = token_store_mutex.lock().expect("Failed to acquire lock on token store");
|
|
let request = form.get();
|
|
let client_id = request.client_id.as_ref().unwrap().to_owned();
|
|
|
|
let subject = token_store.get_token(AUTH_CODE_TYPE, &request.code).map(|code| {
|
|
code.subject.to_owned()
|
|
});
|
|
|
|
let server_url = config.url.to_owned().or_else(|| host.map(|host| {
|
|
let host_str: &str = host.into();
|
|
format!("http://{}/", host_str)
|
|
})).expect("Failed to determine server url");
|
|
|
|
subject.map(|sub| {
|
|
let now_seconds = get_time().sec as u64;
|
|
let jwt_header = json!({});
|
|
let jwt_claims = json!({
|
|
"iss": server_url.to_owned(),
|
|
"sub": sub.to_owned(),
|
|
"aud": client_id,
|
|
"exp": now_seconds + 3600,
|
|
"iat": now_seconds,
|
|
});
|
|
|
|
let token = Token {
|
|
access_token: Uuid::new_v4().hyphenated().to_string(),
|
|
token_type: "Bearer".to_owned(),
|
|
refresh_token: Uuid::new_v4().hyphenated().to_string(),
|
|
expires_in: 3600,
|
|
id_token: frank_jwt::encode(
|
|
jwt_header,
|
|
&key.keyfile,
|
|
&jwt_claims,
|
|
frank_jwt::Algorithm::RS256,
|
|
).expect("Failed to sign")
|
|
};
|
|
|
|
token_store.put_token(token::Token {
|
|
token: token.access_token.clone(),
|
|
token_type: ACCESS_TOKEN_TYPE.to_owned(),
|
|
subject: sub.to_owned(),
|
|
expiry: Some(Duration::from_secs(token.expires_in))
|
|
});
|
|
|
|
token_store.put_token(token::Token {
|
|
token: token.refresh_token.clone(),
|
|
token_type: REFRESH_TOKEN_TYPE.to_owned(),
|
|
subject: token.access_token.clone(),
|
|
expiry: None
|
|
});
|
|
|
|
token_store.delete_token(AUTH_CODE_TYPE, &request.code);
|
|
|
|
Json(token)
|
|
})
|
|
}
|
|
|
|
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
|
#[get("/oidc/userinfo")]
|
|
fn userinfo_get(token_store_mutex: State<Mutex<Box<TokenStore>>>, access_token: BearerToken) -> Option<Json<UserInfo>> {
|
|
let token_store = token_store_mutex.lock().expect("Failed to acquire lock on token store");
|
|
userinfo(&*token_store, access_token)
|
|
}
|
|
|
|
#[post("/oidc/userinfo")]
|
|
fn userinfo_post(token_store_mutex: State<Mutex<Box<TokenStore>>>, access_token: BearerToken) -> Option<Json<UserInfo>> {
|
|
let token_store = token_store_mutex.lock().expect("Failed to acquire lock on token store");
|
|
userinfo(&*token_store, access_token)
|
|
}
|
|
|
|
fn userinfo(token_store: &Box<TokenStore>, access_token: BearerToken) -> Option<Json<UserInfo>> {
|
|
token_store.get_token(ACCESS_TOKEN_TYPE, access_token.into()).map(|token| {
|
|
Json(UserInfo {
|
|
sub: token.subject.clone(),
|
|
..Default::default()
|
|
})
|
|
})
|
|
}
|
|
|
|
#[get("/.well-known/openid-configuration")]
|
|
fn configuration(config: State<Config>, host: Option<Host>) -> Json<Configuration> {
|
|
let server_url = config.url.to_owned().or_else(|| host.map(|host| {
|
|
let host_str: &str = host.into();
|
|
format!("http://{}/", host_str)
|
|
})).expect("Failed to determine server url");
|
|
|
|
Json(Configuration {
|
|
issuer: server_url.to_owned(),
|
|
authorization_endpoint: format!("{}oidc/authenticate", server_url),
|
|
token_endpoint: format!("{}oidc/token", server_url),
|
|
userinfo_endpoint: format!("{}oidc/userinfo", server_url),
|
|
jwks_uri: format!("{}oidc/jwks", server_url),
|
|
registration_endpoint: format!("{}oidc/registration", server_url),
|
|
response_types_supported: vec!["code".to_owned()],
|
|
subject_types_supported: vec!["public".to_owned()],
|
|
id_token_signing_alg_values_supported: vec!["RS256".to_owned()],
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[get("/oidc/jwks")]
|
|
fn keys(key: State<Key>) -> Json<JSONWebKeySet> {
|
|
Json(JSONWebKeySet {
|
|
keys: vec![JSONWebKey {
|
|
kty: "RSA".to_owned(),
|
|
alg: Some("RS256".to_owned()),
|
|
key_use: Some("sig".to_owned()),
|
|
kid: Some("key".to_owned()),
|
|
n: key.n(),
|
|
e: key.e(),
|
|
..Default::default()
|
|
}]
|
|
})
|
|
}
|
|
|
|
pub fn routes() -> Vec<Route> {
|
|
routes![authenticate_post, authenticate_get, token, userinfo_post, userinfo_get, configuration, keys]
|
|
}
|