Domain-locked web tokens. (#5894)
* Domain-locking web tokens. * JS part. * Fix linting issues.
This commit is contained in:
parent
4d5280e43c
commit
53609f703e
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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.',
|
||||||
|
@ -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);
|
||||||
})
|
})
|
||||||
|
@ -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');
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>>) {
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user