blob: 99392f1138955ca7da72abc0c75693aa98e1ddbd [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::constants::USER_INFO_URI;
use crate::error::{ResultExt, TokenProviderError};
use crate::http::{HttpClient, HttpRequest, HttpRequestBuilder};
use crate::oauth::{self, AccessToken, RefreshToken};
use crate::time::Clock;
use fidl_fuchsia_identity_external::{
Error as ApiError, OauthOpenIdConnectRequest, OauthOpenIdConnectRequestStream,
OpenIdTokenFromOauthRefreshTokenRequest, OpenIdUserInfoFromOauthAccessTokenRequest,
};
use fidl_fuchsia_identity_tokens::{OpenIdToken, OpenIdUserInfo};
use fuchsia_zircon::Duration;
use futures::prelude::*;
use hyper::StatusCode;
use log::warn;
use serde::Deserialize;
use serde_json::from_str;
use std::marker::PhantomData;
type TokenProviderResult<T> = Result<T, TokenProviderError>;
/// An implementation of the `fuchsia.identity.external.OauthOpenIdConnect`
/// protocol that communicates with the Google identity system to perform
/// exchanges between Oauth 2.0 and OpenId Connect tokens.
pub struct OauthOpenIdConnect<HC, C>
where
HC: HttpClient,
C: Clock,
{
/// A client used for making HTTP requests.
http_client: HC,
/// A marker denoting which clock implementation to use.
_clock: PhantomData<C>,
}
impl<HC, C> OauthOpenIdConnect<HC, C>
where
HC: HttpClient,
C: Clock,
{
/// Create a new GoogleAuthProvider.
pub fn new(http_client: HC) -> Self {
OauthOpenIdConnect { http_client, _clock: PhantomData }
}
/// Handle requests passed to the supplied stream.
pub async fn handle_requests_from_stream(
&self,
mut stream: OauthOpenIdConnectRequestStream,
) -> Result<(), fidl::Error> {
while let Some(request) = stream.try_next().await? {
self.handle_request(request).await?;
}
Ok(())
}
async fn handle_request(&self, request: OauthOpenIdConnectRequest) -> Result<(), fidl::Error> {
match request {
OauthOpenIdConnectRequest::GetIdTokenFromRefreshToken { request, responder } => {
let mut response =
self.get_id_token_from_refresh_token(request).await.map_err(|e| e.api_error);
responder.send(&mut response).map_err(|e| {
warn!("Error sending response for GetIdTokenFromRefreshToken: {:?}", e);
e
})
}
OauthOpenIdConnectRequest::GetUserInfoFromAccessToken { request, responder } => {
let mut response =
self.get_user_info_from_access_token(request).await.map_err(|e| e.api_error);
responder.send(&mut response).map_err(|e| {
warn!("Error sending response for GetUserInfoFromAccessToken: {:?}", e);
e
})
}
}
}
async fn get_id_token_from_refresh_token(
&self,
request: OpenIdTokenFromOauthRefreshTokenRequest,
) -> TokenProviderResult<OpenIdToken> {
let OpenIdTokenFromOauthRefreshTokenRequest { refresh_token, audiences, .. } = request;
let refresh_token_contents = refresh_token
.ok_or(TokenProviderError::new(ApiError::InvalidRequest))?
.content
.ok_or(TokenProviderError::new(ApiError::InvalidRequest))?;
if refresh_token_contents.is_empty() {
return Err(TokenProviderError::new(ApiError::InvalidRequest));
}
let audience = match audiences {
None => None,
Some(mut audiences) => match audiences.len() {
0 => None,
1 => Some(audiences.remove(0)),
// TODO(satsukiu): support requests for multiple audiences
_ => return Err(TokenProviderError::new(ApiError::UnsupportedOperation)),
},
};
match audience.as_ref() {
Some(aud) if aud.is_empty() => {
return Err(TokenProviderError::new(ApiError::InvalidRequest))
}
_ => (),
}
let request = build_id_token_request(RefreshToken(refresh_token_contents), audience)?;
let (response_body, status) = self.http_client.request(request).await?;
let (id_token, expires_in) = parse_id_token_response(response_body, status)?;
let expiry_time = C::current_time() + expires_in;
Ok(OpenIdToken {
content: Some(id_token.0),
expiry_time: Some(expiry_time.into_nanos()),
..OpenIdToken::EMPTY
})
}
async fn get_user_info_from_access_token(
&self,
request: OpenIdUserInfoFromOauthAccessTokenRequest,
) -> TokenProviderResult<OpenIdUserInfo> {
let access_token_content = request
.access_token
.ok_or(TokenProviderError::new(ApiError::InvalidRequest))?
.content
.ok_or(TokenProviderError::new(ApiError::InvalidRequest))?;
if access_token_content.is_empty() {
return Err(TokenProviderError::new(ApiError::InvalidRequest));
}
let request = build_user_info_request(&AccessToken(access_token_content))?;
let (response_body, status_code) = self.http_client.request(request).await?;
let OpenIdUserInfoResponse { sub, name, email, picture, .. } =
parse_user_info_response(response_body, status_code)?;
Ok(OpenIdUserInfo { subject: Some(sub), name, email, picture, ..OpenIdUserInfo::EMPTY })
}
}
#[derive(Debug, PartialEq)]
struct IdToken(pub String);
/// Response type for OpenID user info requests.
#[derive(Debug, Deserialize, PartialEq)]
pub struct OpenIdUserInfoResponse {
pub sub: String,
pub name: Option<String>,
pub email: Option<String>,
pub picture: Option<String>,
}
/// Response type for an OpenID ID token request.
#[derive(Debug, Deserialize)]
struct OpenIdTokenResponse {
pub id_token: String,
pub expires_in: u32,
}
/// Error response for OpenID requests.
#[derive(Debug, Deserialize)]
struct OpenIdErrorResponse {
pub error: String,
}
/// Construct an `HttpRequest` for an OpenID user info request.
pub fn build_user_info_request(access_token: &AccessToken) -> TokenProviderResult<HttpRequest> {
HttpRequestBuilder::new(USER_INFO_URI.as_str(), "GET")
.with_header("Authorization", format!("Bearer {}", access_token.0))
.finish()
}
/// Construct an `HttpRequest` to request an OpenID ID token.
fn build_id_token_request(
refresh_token: RefreshToken,
audience: Option<String>,
) -> TokenProviderResult<HttpRequest> {
// OpenID standard dictates that id_token is returned as part of an Oauth
// access token response. Thus, the request is really just an Oauth request.
oauth::build_request_with_refresh_token(refresh_token, vec![], audience)
}
/// Parse an OpenID user info response.
pub fn parse_user_info_response(
response_body: Option<String>,
status_code: StatusCode,
) -> TokenProviderResult<OpenIdUserInfoResponse> {
match (response_body.as_ref(), status_code) {
(Some(response), StatusCode::OK) => {
serde_json::from_str::<OpenIdUserInfoResponse>(&response)
.token_provider_error(ApiError::Server)
}
(Some(response), status) if status.is_client_error() => {
let error_response = from_str::<OpenIdErrorResponse>(&response)
.token_provider_error(ApiError::Server)?;
warn!("Got unexpected error code for OpenId user info: {}", error_response.error);
Err(TokenProviderError::new(ApiError::Server))
}
_ => Err(TokenProviderError::new(ApiError::Server)),
}
}
/// Parse an OpenID ID token response.
fn parse_id_token_response(
response_body: Option<String>,
status_code: StatusCode,
) -> TokenProviderResult<(IdToken, Duration)> {
match (response_body.as_ref(), status_code) {
(Some(response), StatusCode::OK) => {
let OpenIdTokenResponse { id_token, expires_in } =
serde_json::from_str::<OpenIdTokenResponse>(&response)
.token_provider_error(ApiError::Server)?;
Ok((IdToken(id_token), Duration::from_seconds(expires_in as i64)))
}
(Some(response), status) if status.is_client_error() => {
let error_response = from_str::<OpenIdErrorResponse>(&response)
.token_provider_error(ApiError::Server)?;
warn!("Got unexpected error code while retrieving ID token: {}", error_response.error);
Err(TokenProviderError::new(ApiError::Server))
}
_ => Err(TokenProviderError::new(ApiError::Server)),
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::http::mock::TestHttpClient;
use crate::time::mock::{FixedClock, TEST_CURRENT_TIME};
use fidl::endpoints::create_proxy_and_stream;
use fidl_fuchsia_identity_external::{OauthOpenIdConnectMarker, OauthOpenIdConnectProxy};
use fidl_fuchsia_identity_tokens::{OauthAccessToken, OauthRefreshToken};
use fuchsia_async as fasync;
use fuchsia_zircon::Duration;
use futures::future::join;
use hyper::StatusCode;
async fn run_proxy_test<Fn, Fut>(
test_object: OauthOpenIdConnect<TestHttpClient, FixedClock>,
test_fn: Fn,
) where
Fn: FnOnce(OauthOpenIdConnectProxy) -> Fut,
Fut: Future<Output = Result<(), fidl::Error>>,
{
let (proxy, stream) = create_proxy_and_stream::<OauthOpenIdConnectMarker>().unwrap();
let server_fut = test_object.handle_requests_from_stream(stream);
let test_fut = test_fn(proxy);
let (test_res, server_res) = join(test_fut, server_fut).await;
assert!(test_res.is_ok());
assert!(server_res.is_ok());
}
fn create_refresh_token(content: &str) -> OauthRefreshToken {
OauthRefreshToken {
content: Some(content.to_string()),
account_id: None,
..OauthRefreshToken::EMPTY
}
}
fn create_access_token(content: &str) -> OauthAccessToken {
OauthAccessToken {
content: Some(content.to_string()),
expiry_time: None,
..OauthAccessToken::EMPTY
}
}
#[fasync::run_until_stalled(test)]
async fn test_get_id_token_success() {
let http_result = "{\"id_token\": \"test-id-token\", \"expires_in\": 3600}";
let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::OK);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let id_token = proxy
.get_id_token_from_refresh_token(OpenIdTokenFromOauthRefreshTokenRequest {
refresh_token: Some(create_refresh_token("refresh_token")),
audiences: None,
..OpenIdTokenFromOauthRefreshTokenRequest::EMPTY
})
.await?
.unwrap();
assert_eq!(id_token.content, Some("test-id-token".to_string()),);
assert_eq!(
id_token.expiry_time.unwrap(),
(TEST_CURRENT_TIME.clone() + Duration::from_seconds(3600)).into_nanos()
);
Ok(())
})
.await;
// An empty vector for audiences is also accepted as default audience.
let http_result = "{\"id_token\": \"test-id-token\", \"expires_in\": 3600}";
let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::OK);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let id_token = proxy
.get_id_token_from_refresh_token(OpenIdTokenFromOauthRefreshTokenRequest {
refresh_token: Some(create_refresh_token("refresh_token")),
audiences: Some(vec![]),
..OpenIdTokenFromOauthRefreshTokenRequest::EMPTY
})
.await?
.unwrap();
assert_eq!(id_token.content, Some("test-id-token".to_string()),);
assert_eq!(
id_token.expiry_time.unwrap(),
(TEST_CURRENT_TIME.clone() + Duration::from_seconds(3600)).into_nanos()
);
Ok(())
})
.await;
}
#[fasync::run_until_stalled(test)]
async fn test_get_id_token_failures() {
// Invalid request
let mock_http = TestHttpClient::with_error(ApiError::Internal);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let request_result = proxy
.get_id_token_from_refresh_token(OpenIdTokenFromOauthRefreshTokenRequest {
refresh_token: Some(create_refresh_token("refresh_token")),
audiences: Some(vec!["".to_string()]),
..OpenIdTokenFromOauthRefreshTokenRequest::EMPTY
})
.await?;
assert_eq!(request_result, Err(ApiError::InvalidRequest));
Ok(())
})
.await;
// Error response
let http_result = "{\"error\": \"invalid_client\"}";
let mock_http = TestHttpClient::with_response(Some(http_result), StatusCode::BAD_REQUEST);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let request_result = proxy
.get_id_token_from_refresh_token(OpenIdTokenFromOauthRefreshTokenRequest {
refresh_token: Some(create_refresh_token("refresh_token")),
audiences: None,
..OpenIdTokenFromOauthRefreshTokenRequest::EMPTY
})
.await?;
assert_eq!(request_result, Err(ApiError::Server));
Ok(())
})
.await;
// Network error
let mock_http = TestHttpClient::with_error(ApiError::Network);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let request_result = proxy
.get_id_token_from_refresh_token(OpenIdTokenFromOauthRefreshTokenRequest {
refresh_token: Some(create_refresh_token("refresh_token")),
audiences: None,
..OpenIdTokenFromOauthRefreshTokenRequest::EMPTY
})
.await?;
assert_eq!(request_result, Err(ApiError::Network));
Ok(())
})
.await;
}
#[fasync::run_until_stalled(test)]
async fn test_get_user_info_success() {
let http_response =
"{\"sub\": \"test-id\", \"name\": \"Bill\", \"profile\": \"profile-url\", \
\"picture\": \"picture-url\", \"email\": \"bill@test.com\"}";
let mock_http = TestHttpClient::with_response(Some(http_response), StatusCode::OK);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let user_info = proxy
.get_user_info_from_access_token(OpenIdUserInfoFromOauthAccessTokenRequest {
access_token: Some(create_access_token("access_token")),
..OpenIdUserInfoFromOauthAccessTokenRequest::EMPTY
})
.await?
.unwrap();
assert_eq!(
user_info,
OpenIdUserInfo {
subject: Some("test-id".to_string()),
name: Some("Bill".to_string()),
email: Some("bill@test.com".to_string()),
picture: Some("picture-url".to_string()),
..OpenIdUserInfo::EMPTY
}
);
Ok(())
})
.await;
}
#[fasync::run_until_stalled(test)]
async fn test_get_user_info_failures() {
// Invalid request
let mock_http = TestHttpClient::with_error(ApiError::Internal);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let request_result = proxy
.get_user_info_from_access_token(OpenIdUserInfoFromOauthAccessTokenRequest {
access_token: Some(create_access_token("")),
..OpenIdUserInfoFromOauthAccessTokenRequest::EMPTY
})
.await?;
assert_eq!(request_result, Err(ApiError::InvalidRequest));
Ok(())
})
.await;
// Error response
let mock_http = TestHttpClient::with_response(None, StatusCode::INTERNAL_SERVER_ERROR);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let request_result = proxy
.get_user_info_from_access_token(OpenIdUserInfoFromOauthAccessTokenRequest {
access_token: Some(create_access_token("access_token")),
..OpenIdUserInfoFromOauthAccessTokenRequest::EMPTY
})
.await?;
assert_eq!(request_result, Err(ApiError::Server));
Ok(())
})
.await;
// Network error
let mock_http = TestHttpClient::with_error(ApiError::Network);
let test_object = OauthOpenIdConnect::new(mock_http);
run_proxy_test(test_object, |proxy| async move {
let request_result = proxy
.get_user_info_from_access_token(OpenIdUserInfoFromOauthAccessTokenRequest {
access_token: Some(create_access_token("access_token")),
..OpenIdUserInfoFromOauthAccessTokenRequest::EMPTY
})
.await?;
assert_eq!(request_result, Err(ApiError::Network));
Ok(())
})
.await;
}
#[test]
fn test_parse_user_info_success() {
// All optional arguments returned
let http_result = String::from(
"{\"sub\": \"id-123\", \"name\": \"Amanda\", \"profile\": \"profile-url\", \
\"picture\": \"picture-url\", \"email\": \"id-123@example.com\"}",
);
let user_info_response =
parse_user_info_response(Some(http_result), StatusCode::OK).unwrap();
assert_eq!(
user_info_response,
OpenIdUserInfoResponse {
sub: String::from("id-123"),
name: Some(String::from("Amanda")),
email: Some(String::from("id-123@example.com")),
picture: Some(String::from("picture-url")),
}
);
// Only ID provided
let http_result = String::from("{\"sub\": \"id-321\"}");
let user_info_response =
parse_user_info_response(Some(http_result), StatusCode::OK).unwrap();
assert_eq!(
user_info_response,
OpenIdUserInfoResponse {
sub: String::from("id-321"),
name: None,
email: None,
picture: None,
}
);
}
#[test]
fn test_parse_user_info_failures() {
// Bad token case
let invalid_http_result = String::from("{\"error\": \"invalid_token\"}");
let result = parse_user_info_response(Some(invalid_http_result), StatusCode::UNAUTHORIZED);
assert_eq!(result.unwrap_err().api_error, ApiError::Server);
// Server error case
let result = parse_user_info_response(None, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(result.unwrap_err().api_error, ApiError::Server);
// Malformed response case
let invalid_http_result = String::from("\\\\malformed\\\\");
let result = parse_user_info_response(Some(invalid_http_result), StatusCode::OK);
assert_eq!(result.unwrap_err().api_error, ApiError::Server);
}
#[test]
fn test_parse_id_token_response_success() {
let http_result = "{\"id_token\": \"test-id-token\", \"expires_in\": 3600}".to_string();
let (id_token, expires_in) =
parse_id_token_response(Some(http_result), StatusCode::OK).unwrap();
assert_eq!(id_token, IdToken("test-id-token".to_string()));
assert_eq!(expires_in, Duration::from_seconds(3600));
}
#[test]
fn test_parse_id_token_response_failures() {
// Bad token case
let invalid_http_result = "{\"error\": \"invalid_token\"}".to_string();
let result = parse_id_token_response(Some(invalid_http_result), StatusCode::UNAUTHORIZED);
assert_eq!(result.unwrap_err().api_error, ApiError::Server);
// Server error case
let result = parse_id_token_response(None, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(result.unwrap_err().api_error, ApiError::Server);
// Malformed response case
let invalid_http_result = "\\\\malformed\\\\".to_string();
let result = parse_id_token_response(Some(invalid_http_result), StatusCode::OK);
assert_eq!(result.unwrap_err().api_error, ApiError::Server);
}
}