Domain-locked web tokens. (#5894)

* Domain-locking web tokens.

* JS part.

* Fix linting issues.
This commit is contained in:
Tomasz Drwięga 2017-06-22 20:05:40 +02:00 committed by Gav Wood
parent 4d5280e43c
commit 53609f703e
13 changed files with 85 additions and 48 deletions

View File

@ -71,7 +71,7 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::collections::HashMap; use std::collections::HashMap;
use jsonrpc_http_server::{self as http, hyper}; use jsonrpc_http_server::{self as http, hyper, Origin};
use fetch::Fetch; use fetch::Fetch;
use parity_reactor::Remote; use parity_reactor::Remote;
@ -90,12 +90,12 @@ impl<F> SyncStatus for F where F: Fn() -> bool + Send + Sync {
/// Validates Web Proxy tokens /// Validates Web Proxy tokens
pub trait WebProxyTokens: Send + Sync { pub trait WebProxyTokens: Send + Sync {
/// Should return true if token is a valid web proxy access token. /// Should return a domain allowed to be accessed by this token or `None` if the token is not valid
fn is_web_proxy_token_valid(&self, token: &str) -> bool; fn domain(&self, token: &str) -> Option<Origin>;
} }
impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync { impl<F> WebProxyTokens for F where F: Fn(String) -> Option<Origin> + Send + Sync {
fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) } fn domain(&self, token: &str) -> Option<Origin> { self(token.to_owned()) }
} }
/// Current supported endpoints. /// Current supported endpoints.

View File

@ -312,7 +312,7 @@ fn should_encode_and_decode_base32() {
#[test] #[test]
fn should_stream_web_content() { fn should_stream_web_content() {
// given // given
let (server, fetch) = serve_with_fetch("token"); let (server, fetch) = serve_with_fetch("token", "https://parity.io");
// when // when
let response = request(server, let response = request(server,
@ -335,7 +335,7 @@ fn should_stream_web_content() {
#[test] #[test]
fn should_support_base32_encoded_web_urls() { fn should_support_base32_encoded_web_urls() {
// given // given
let (server, fetch) = serve_with_fetch("token"); let (server, fetch) = serve_with_fetch("token", "https://parity.io");
// when // when
let response = request(server, let response = request(server,
@ -358,7 +358,7 @@ fn should_support_base32_encoded_web_urls() {
#[test] #[test]
fn should_correctly_handle_long_label_when_splitted() { fn should_correctly_handle_long_label_when_splitted() {
// given // given
let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL"); let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL", "https://contribution.melonport.com");
// when // when
let response = request(server, let response = request(server,
@ -382,7 +382,7 @@ fn should_correctly_handle_long_label_when_splitted() {
#[test] #[test]
fn should_support_base32_encoded_web_urls_as_path() { fn should_support_base32_encoded_web_urls_as_path() {
// given // given
let (server, fetch) = serve_with_fetch("token"); let (server, fetch) = serve_with_fetch("token", "https://parity.io");
// when // when
let response = request(server, let response = request(server,
@ -402,10 +402,32 @@ fn should_support_base32_encoded_web_urls_as_path() {
fetch.assert_no_more_requests(); fetch.assert_no_more_requests();
} }
#[test]
fn should_return_error_on_non_whitelisted_domain() {
// given
let (server, fetch) = serve_with_fetch("token", "https://ethcore.io");
// when
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\
Connection: close\r\n\
\r\n\
"
);
// then
response.assert_status("HTTP/1.1 400 Bad Request");
assert_security_headers_for_embed(&response.headers);
fetch.assert_no_more_requests();
}
#[test] #[test]
fn should_return_error_on_invalid_token() { fn should_return_error_on_invalid_token() {
// given // given
let (server, fetch) = serve_with_fetch("test"); let (server, fetch) = serve_with_fetch("test", "https://parity.io");
// when // when
let response = request(server, let response = request(server,
@ -427,7 +449,7 @@ fn should_return_error_on_invalid_token() {
#[test] #[test]
fn should_return_error_on_invalid_protocol() { fn should_return_error_on_invalid_protocol() {
// given // given
let (server, fetch) = serve_with_fetch("token"); let (server, fetch) = serve_with_fetch("token", "ftp://parity.io");
// when // when
let response = request(server, let response = request(server,
@ -449,7 +471,7 @@ fn should_return_error_on_invalid_protocol() {
#[test] #[test]
fn should_disallow_non_get_requests() { fn should_disallow_non_get_requests() {
// given // given
let (server, fetch) = serve_with_fetch("token"); let (server, fetch) = serve_with_fetch("token", "https://parity.io");
// when // when
let response = request(server, let response = request(server,
@ -474,7 +496,7 @@ fn should_disallow_non_get_requests() {
#[test] #[test]
fn should_fix_absolute_requests_based_on_referer() { fn should_fix_absolute_requests_based_on_referer() {
// given // given
let (server, fetch) = serve_with_fetch("token"); let (server, fetch) = serve_with_fetch("token", "https://parity.io");
// when // when
let response = request(server, let response = request(server,
@ -497,7 +519,7 @@ fn should_fix_absolute_requests_based_on_referer() {
#[test] #[test]
fn should_fix_absolute_requests_based_on_referer_in_url() { fn should_fix_absolute_requests_based_on_referer_in_url() {
// given // given
let (server, fetch) = serve_with_fetch("token"); let (server, fetch) = serve_with_fetch("token", "https://parity.io");
// when // when
let response = request(server, let response = request(server,

View File

@ -100,13 +100,15 @@ pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Serv
(server, fetch, reg) (server, fetch, reg)
} }
pub fn serve_with_fetch(web_token: &'static str) -> (Server, FakeFetch) { pub fn serve_with_fetch(web_token: &'static str, domain: &'static str) -> (Server, FakeFetch) {
let fetch = FakeFetch::default(); let fetch = FakeFetch::default();
let f = fetch.clone(); let f = fetch.clone();
let (server, _) = init_server(move |builder| { let (server, _) = init_server(move |builder| {
builder builder
.fetch(f.clone()) .fetch(f.clone())
.web_proxy_tokens(Arc::new(move |token| &token == web_token)) .web_proxy_tokens(Arc::new(move |token| {
if &token == web_token { Some(domain.into()) } else { None }
}))
}, Default::default(), Remote::new_sync()); }, Default::default(), Remote::new_sync());
(server, fetch) (server, fetch)
@ -147,7 +149,7 @@ impl ServerBuilder {
dapps_path: dapps_path.as_ref().to_owned(), dapps_path: dapps_path.as_ref().to_owned(),
registrar: registrar, registrar: registrar,
sync_status: Arc::new(|| false), sync_status: Arc::new(|| false),
web_proxy_tokens: Arc::new(|_| false), web_proxy_tokens: Arc::new(|_| None),
signer_address: None, signer_address: None,
allowed_hosts: DomainsValidation::Disabled, allowed_hosts: DomainsValidation::Disabled,
remote: remote, remote: remote,

View File

@ -133,14 +133,14 @@ impl<F: Fetch> WebHandler<F> {
let target_url = token_it.next(); let target_url = token_it.next();
// Check if token supplied in URL is correct. // Check if token supplied in URL is correct.
match token { let domain = match token.and_then(|token| self.web_proxy_tokens.domain(token)) {
Some(token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {}, Some(domain) => domain,
_ => { _ => {
return Err(State::Error(ContentHandler::error( return Err(State::Error(ContentHandler::error(
StatusCode::BadRequest, "Invalid Access Token", "Invalid or old web proxy access token supplied.", Some("Try refreshing the page."), self.embeddable_on.clone() StatusCode::BadRequest, "Invalid Access Token", "Invalid or old web proxy access token supplied.", Some("Try refreshing the page."), self.embeddable_on.clone()
))); )));
} }
} };
// Validate protocol // Validate protocol
let mut target_url = match target_url { let mut target_url = match target_url {
@ -152,6 +152,12 @@ impl<F: Fetch> WebHandler<F> {
} }
}; };
if !target_url.starts_with(&*domain) {
return Err(State::Error(ContentHandler::error(
StatusCode::BadRequest, "Invalid Domain", "Dapp attempted to access invalid domain.", Some(&target_url), self.embeddable_on.clone(),
)));
}
if !target_url.ends_with("/") { if !target_url.ends_with("/") {
target_url = format!("{}/", target_url); target_url = format!("{}/", target_url);
} }

View File

@ -42,9 +42,9 @@ export default class Signer {
.execute('signer_generateAuthorizationToken'); .execute('signer_generateAuthorizationToken');
} }
generateWebProxyAccessToken () { generateWebProxyAccessToken (domain) {
return this._transport return this._transport
.execute('signer_generateWebProxyAccessToken'); .execute('signer_generateWebProxyAccessToken', domain);
} }
rejectRequest (requestId) { rejectRequest (requestId) {

View File

@ -30,7 +30,11 @@ export default {
generateWebProxyAccessToken: { generateWebProxyAccessToken: {
desc: 'Generates a new web proxy access token.', desc: 'Generates a new web proxy access token.',
params: [], params: [{
type: String,
desc: 'Domain for which the token is valid. Only requests to this domain will be allowed.',
example: 'https://parity.io'
}],
returns: { returns: {
type: String, type: String,
desc: 'The new web proxy access token.', desc: 'The new web proxy access token.',

View File

@ -59,15 +59,17 @@ export default class Store {
} }
@action gotoUrl = (_url) => { @action gotoUrl = (_url) => {
transaction(() => { let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
let url = (_url || this.nextUrl).trim().replace(/\/+$/, '');
if (!hasProtocol.test(url)) { if (!hasProtocol.test(url)) {
url = `https://${url}`; url = `https://${url}`;
} }
this.setNextUrl(url); return this.generateToken(url).then(() => {
this.setCurrentUrl(this.nextUrl); transaction(() => {
this.setNextUrl(url);
this.setCurrentUrl(this.nextUrl);
});
}); });
} }
@ -134,11 +136,11 @@ export default class Store {
this.nextUrl = url; this.nextUrl = url;
} }
generateToken = () => { generateToken = (_url) => {
this.setToken(null); this.setToken(null);
return this._api.signer return this._api.signer
.generateWebProxyAccessToken() .generateWebProxyAccessToken(_url)
.then((token) => { .then((token) => {
this.setToken(token); this.setToken(token);
}) })

View File

@ -62,15 +62,16 @@ describe('views/Web/Store', () => {
describe('gotoUrl', () => { describe('gotoUrl', () => {
it('uses the nextUrl when none specified', () => { it('uses the nextUrl when none specified', () => {
store.setNextUrl('https://parity.io'); store.setNextUrl('https://parity.io');
store.gotoUrl();
expect(store.currentUrl).to.equal('https://parity.io'); return store.gotoUrl().then(() => {
expect(store.currentUrl).to.equal('https://parity.io');
});
}); });
it('adds https when no protocol', () => { it('adds https when no protocol', () => {
store.gotoUrl('google.com'); return store.gotoUrl('google.com').then(() => {
expect(store.currentUrl).to.equal('https://google.com');
expect(store.currentUrl).to.equal('https://google.com'); });
}); });
}); });

View File

@ -37,7 +37,6 @@ export default class Web extends Component {
componentDidMount () { componentDidMount () {
this.store.gotoUrl(this.props.params.url); this.store.gotoUrl(this.props.params.url);
return this.store.generateToken();
} }
componentWillReceiveProps (props) { componentWillReceiveProps (props) {

View File

@ -232,7 +232,7 @@ mod server {
) -> Result<Middleware, String> { ) -> Result<Middleware, String> {
let signer = deps.signer; let signer = deps.signer;
let parity_remote = parity_reactor::Remote::new(deps.remote.clone()); let parity_remote = parity_reactor::Remote::new(deps.remote.clone());
let web_proxy_tokens = Arc::new(move |token| signer.is_valid_web_proxy_access_token(&token)); let web_proxy_tokens = Arc::new(move |token| signer.web_proxy_access_token_domain(&token));
Ok(parity_dapps::Middleware::dapps( Ok(parity_dapps::Middleware::dapps(
parity_remote, parity_remote,

View File

@ -16,6 +16,7 @@
use std::sync::Arc; use std::sync::Arc;
use std::ops::Deref; use std::ops::Deref;
use http::Origin;
use util::Mutex; use util::Mutex;
use transient_hashmap::TransientHashMap; use transient_hashmap::TransientHashMap;
@ -29,7 +30,7 @@ const TOKEN_LIFETIME_SECS: u32 = 3600;
pub struct SignerService { pub struct SignerService {
is_enabled: bool, is_enabled: bool,
queue: Arc<ConfirmationsQueue>, queue: Arc<ConfirmationsQueue>,
web_proxy_tokens: Mutex<TransientHashMap<String, ()>>, web_proxy_tokens: Mutex<TransientHashMap<String, Origin>>,
generate_new_token: Box<Fn() -> Result<String, String> + Send + Sync + 'static>, generate_new_token: Box<Fn() -> Result<String, String> + Send + Sync + 'static>,
} }
@ -46,16 +47,16 @@ impl SignerService {
} }
/// Checks if the token is valid web proxy access token. /// Checks if the token is valid web proxy access token.
pub fn is_valid_web_proxy_access_token(&self, token: &String) -> bool { pub fn web_proxy_access_token_domain(&self, token: &String) -> Option<Origin> {
self.web_proxy_tokens.lock().contains_key(&token) self.web_proxy_tokens.lock().get(token).cloned()
} }
/// Generates a new web proxy access token. /// Generates a new web proxy access token.
pub fn generate_web_proxy_access_token(&self) -> String { pub fn generate_web_proxy_access_token(&self, domain: Origin) -> String {
let token = random_string(16); let token = random_string(16);
let mut tokens = self.web_proxy_tokens.lock(); let mut tokens = self.web_proxy_tokens.lock();
tokens.prune(); tokens.prune();
tokens.insert(token.clone(), ()); tokens.insert(token.clone(), domain);
token token
} }

View File

@ -245,8 +245,8 @@ impl<D: Dispatcher + 'static> Signer for SignerClient<D> {
.map_err(|e| errors::token(e)) .map_err(|e| errors::token(e))
} }
fn generate_web_proxy_token(&self) -> Result<String, Error> { fn generate_web_proxy_token(&self, domain: String) -> Result<String, Error> {
Ok(self.signer.generate_web_proxy_access_token()) Ok(self.signer.generate_web_proxy_access_token(domain.into()))
} }
fn subscribe_pending(&self, _meta: Self::Metadata, sub: Subscriber<Vec<ConfirmationRequest>>) { fn subscribe_pending(&self, _meta: Self::Metadata, sub: Subscriber<Vec<ConfirmationRequest>>) {

View File

@ -51,9 +51,9 @@ build_rpc_trait! {
#[rpc(name = "signer_generateAuthorizationToken")] #[rpc(name = "signer_generateAuthorizationToken")]
fn generate_token(&self) -> Result<String, Error>; fn generate_token(&self) -> Result<String, Error>;
/// Generates new web proxy access token. /// Generates new web proxy access token for particular domain.
#[rpc(name = "signer_generateWebProxyAccessToken")] #[rpc(name = "signer_generateWebProxyAccessToken")]
fn generate_web_proxy_token(&self) -> Result<String, Error>; fn generate_web_proxy_token(&self, String) -> Result<String, Error>;
#[pubsub(name = "signer_pending")] { #[pubsub(name = "signer_pending")] {
/// Subscribe to new pending requests on signer interface. /// Subscribe to new pending requests on signer interface.