Identity provider and SSO for Kupolo.
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

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]
}