diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/credentialmanagement/identity | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
44 files changed, 2565 insertions, 0 deletions
diff --git a/dom/credentialmanagement/identity/IPCIdentityCredential.ipdlh b/dom/credentialmanagement/identity/IPCIdentityCredential.ipdlh new file mode 100644 index 0000000000..3502a58b4f --- /dev/null +++ b/dom/credentialmanagement/identity/IPCIdentityCredential.ipdlh @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +namespace mozilla { +namespace dom { + +struct IPCIdentityCredential +{ + nsString id; + nsString type; + nsString token; +}; + +} +} diff --git a/dom/credentialmanagement/identity/IdentityCredential.cpp b/dom/credentialmanagement/identity/IdentityCredential.cpp new file mode 100644 index 0000000000..c901e7d4ed --- /dev/null +++ b/dom/credentialmanagement/identity/IdentityCredential.cpp @@ -0,0 +1,1001 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/IdentityCredential.h" +#include "mozilla/dom/IdentityNetworkHelpers.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/Components.h" +#include "mozilla/ExpandedPrincipal.h" +#include "mozilla/NullPrincipal.h" +#include "nsEffectiveTLDService.h" +#include "nsIGlobalObject.h" +#include "nsIIdentityCredentialPromptService.h" +#include "nsIIdentityCredentialStorageService.h" +#include "nsITimer.h" +#include "nsIXPConnect.h" +#include "nsNetUtil.h" +#include "nsStringStream.h" +#include "nsURLHelper.h" + +namespace mozilla::dom { + +IdentityCredential::~IdentityCredential() = default; + +JSObject* IdentityCredential::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return IdentityCredential_Binding::Wrap(aCx, this, aGivenProto); +} + +IdentityCredential::IdentityCredential(nsPIDOMWindowInner* aParent) + : Credential(aParent) {} + +void IdentityCredential::CopyValuesFrom(const IPCIdentityCredential& aOther) { + this->SetToken(aOther.token()); + this->SetId(aOther.id()); + this->SetType(aOther.type()); +} + +IPCIdentityCredential IdentityCredential::MakeIPCIdentityCredential() { + nsString token, id, type; + GetToken(token); + GetId(id); + GetType(type); + IPCIdentityCredential result; + result.token() = token; + result.id() = id; + result.type() = type; + return result; +} + +void IdentityCredential::GetToken(nsAString& aToken) const { + aToken.Assign(mToken); +} +void IdentityCredential::SetToken(const nsAString& aToken) { + mToken.Assign(aToken); +} + +// static +RefPtr<IdentityCredential::GetIdentityCredentialPromise> +IdentityCredential::DiscoverFromExternalSource( + nsPIDOMWindowInner* aParent, const CredentialRequestOptions& aOptions, + bool aSameOriginWithAncestors) { + MOZ_ASSERT(XRE_IsContentProcess()); + MOZ_ASSERT(aParent); + // Prevent origin confusion by requiring no cross domain iframes + // in this one's ancestry + if (!aSameOriginWithAncestors) { + return IdentityCredential::GetIdentityCredentialPromise::CreateAndReject( + NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__); + } + + Document* parentDocument = aParent->GetExtantDoc(); + if (!parentDocument) { + return IdentityCredential::GetIdentityCredentialPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + RefPtr<IdentityCredential::GetIdentityCredentialPromise::Private> result = + new IdentityCredential::GetIdentityCredentialPromise::Private(__func__); + + if (StaticPrefs:: + dom_security_credentialmanagement_identity_reject_delay_enabled()) { + // This is used to give the promise the appropriate lifetime so it is not + // freed before the callback below is called. This reference is taken as an + // argument to that callback. + RefPtr<IdentityCredential::GetIdentityCredentialPromise::Private> + forCallbackResult = result; + + RefPtr<nsITimer> timeout; + nsresult rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(timeout), + [](nsITimer* aTimer, void* aClosure) -> void { + auto* promise = static_cast< + IdentityCredential::GetIdentityCredentialPromise::Private*>( + aClosure); + if (!promise->IsResolved()) { + promise->Reject(NS_ERROR_DOM_NETWORK_ERR, __func__); + } + // This releases the promise we forgot when we returned from + // this function and the timer we forgot after we built this + // callback. + NS_RELEASE(promise); + NS_RELEASE(aTimer); + }, + do_AddRef(forCallbackResult).take(), + StaticPrefs:: + dom_security_credentialmanagement_identity_reject_delay_duration_ms(), + nsITimer::TYPE_ONE_SHOT, "IdentityCredentialTimeoutCallback"); + if (NS_WARN_IF(NS_FAILED(rv))) { + result->Reject(NS_ERROR_FAILURE, __func__); + return result.forget(); + } + + // Do not clean this timer when we return form this function. This will be + // done at the end of the callback above. + Unused << timeout.forget(); + } + + // Kick the request off to the main process and translate the result to the + // expected type when we get a result. + MOZ_ASSERT(aOptions.mIdentity.WasPassed()); + RefPtr<WindowGlobalChild> wgc = aParent->GetWindowGlobalChild(); + MOZ_ASSERT(wgc); + RefPtr<IdentityCredential> credential = new IdentityCredential(aParent); + wgc->SendDiscoverIdentityCredentialFromExternalSource( + aOptions.mIdentity.Value()) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [result, + credential](const WindowGlobalChild:: + DiscoverIdentityCredentialFromExternalSourcePromise:: + ResolveValueType& aResult) { + if (aResult.isSome()) { + credential->CopyValuesFrom(aResult.value()); + result->Resolve(credential, __func__); + } else if ( + !StaticPrefs:: + dom_security_credentialmanagement_identity_reject_delay_enabled()) { + result->Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); + } + }, + [result](const WindowGlobalChild:: + DiscoverIdentityCredentialFromExternalSourcePromise:: + RejectValueType& aResult) { + if (!StaticPrefs:: + dom_security_credentialmanagement_identity_reject_delay_enabled()) { + result->Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__); + } + }); + return result.forget(); +} + +// static +RefPtr<IdentityCredential::GetIPCIdentityCredentialPromise> +IdentityCredential::DiscoverFromExternalSourceInMainProcess( + nsIPrincipal* aPrincipal, CanonicalBrowsingContext* aBrowsingContext, + const IdentityCredentialRequestOptions& aOptions) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aBrowsingContext); + + // Make sure we have providers. + if (!aOptions.mProviders.WasPassed() || + aOptions.mProviders.Value().Length() < 1) { + return IdentityCredential::GetIPCIdentityCredentialPromise::CreateAndReject( + NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__); + } + + nsCOMPtr<nsIPrincipal> principal(aPrincipal); + RefPtr<CanonicalBrowsingContext> browsingContext(aBrowsingContext); + + // Have the user choose a provider. + return PromptUserToSelectProvider(aBrowsingContext, + aOptions.mProviders.Value()) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [principal, browsingContext](const IdentityProvider& provider) { + return IdentityCredential::CreateCredential( + principal, browsingContext, provider); + }, + [](nsresult error) { + return IdentityCredential::GetIPCIdentityCredentialPromise:: + CreateAndReject(error, __func__); + }); +} + +// static +RefPtr<IdentityCredential::GetIPCIdentityCredentialPromise> +IdentityCredential::CreateCredential(nsIPrincipal* aPrincipal, + BrowsingContext* aBrowsingContext, + const IdentityProvider& aProvider) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aBrowsingContext); + + nsCOMPtr<nsIPrincipal> argumentPrincipal = aPrincipal; + RefPtr<BrowsingContext> browsingContext(aBrowsingContext); + + return IdentityCredential::CheckRootManifest(aPrincipal, aProvider) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aProvider, argumentPrincipal](bool valid) { + if (valid) { + return IdentityCredential::FetchInternalManifest( + argumentPrincipal, aProvider); + } + return IdentityCredential::GetManifestPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + }, + [](nsresult error) { + return IdentityCredential::GetManifestPromise::CreateAndReject( + error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [argumentPrincipal, + aProvider](const IdentityInternalManifest& manifest) { + return IdentityCredential::FetchAccountList(argumentPrincipal, + aProvider, manifest); + }, + [](nsresult error) { + return IdentityCredential::GetAccountListPromise::CreateAndReject( + error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [argumentPrincipal, browsingContext, aProvider]( + const Tuple<IdentityInternalManifest, IdentityAccountList>& + promiseResult) { + IdentityInternalManifest currentManifest; + IdentityAccountList accountList; + Tie(currentManifest, accountList) = promiseResult; + if (!accountList.mAccounts.WasPassed() || + accountList.mAccounts.Value().Length() == 0) { + return IdentityCredential::GetAccountPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + return PromptUserToSelectAccount(browsingContext, accountList, + currentManifest); + }, + [](nsresult error) { + return IdentityCredential::GetAccountPromise::CreateAndReject( + error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [argumentPrincipal, browsingContext, + aProvider](const Tuple<IdentityInternalManifest, IdentityAccount>& + promiseResult) { + IdentityInternalManifest currentManifest; + IdentityAccount account; + Tie(currentManifest, account) = promiseResult; + return IdentityCredential::PromptUserWithPolicy( + browsingContext, argumentPrincipal, account, currentManifest, + aProvider); + }, + [](nsresult error) { + return IdentityCredential::GetAccountPromise::CreateAndReject( + error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [argumentPrincipal, + aProvider](const Tuple<IdentityInternalManifest, IdentityAccount>& + promiseResult) { + IdentityInternalManifest currentManifest; + IdentityAccount account; + Tie(currentManifest, account) = promiseResult; + return IdentityCredential::FetchToken(argumentPrincipal, aProvider, + currentManifest, account); + }, + [](nsresult error) { + return IdentityCredential::GetTokenPromise::CreateAndReject( + error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aProvider]( + const Tuple<IdentityToken, IdentityAccount>& promiseResult) { + IdentityToken token; + IdentityAccount account; + Tie(token, account) = promiseResult; + IPCIdentityCredential credential; + credential.token() = token.mToken; + credential.id() = account.mId; + credential.type() = u"identity"_ns; + return IdentityCredential::GetIPCIdentityCredentialPromise:: + CreateAndResolve(credential, __func__); + }, + [browsingContext](nsresult error) { + CloseUserInterface(browsingContext); + return IdentityCredential::GetIPCIdentityCredentialPromise:: + CreateAndReject(error, __func__); + }); +} + +// static +RefPtr<IdentityCredential::ValidationPromise> +IdentityCredential::CheckRootManifest(nsIPrincipal* aPrincipal, + const IdentityProvider& aProvider) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Build the URL + nsString configLocation = aProvider.mConfigURL; + nsCOMPtr<nsIURI> configURI; + nsresult rv = NS_NewURI(getter_AddRefs(configURI), configLocation); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::ValidationPromise::CreateAndReject(rv, __func__); + } + RefPtr<nsEffectiveTLDService> etld = nsEffectiveTLDService::GetInstance(); + if (!etld) { + return IdentityCredential::ValidationPromise::CreateAndReject( + NS_ERROR_SERVICE_NOT_AVAILABLE, __func__); + } + nsCString manifestURIString; + rv = etld->GetSite(configURI, manifestURIString); + if (NS_FAILED(rv)) { + return IdentityCredential::ValidationPromise::CreateAndReject( + NS_ERROR_INVALID_ARG, __func__); + } + manifestURIString.AppendLiteral("/.well-known/web-identity"); + + // Create the global + RefPtr<NullPrincipal> nullPrincipal = + NullPrincipal::CreateWithInheritedAttributes(aPrincipal); + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr<nsIGlobalObject> global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> sandbox(cx); + rv = xpc->CreateSandbox(cx, nullPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::ValidationPromise::CreateAndReject(rv, __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::ValidationPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr<InternalRequest>(manifestURIString, fragment); + internalRequest->SetCredentialsMode(RequestCredentials::Omit); + internalRequest->SetReferrerPolicy(ReferrerPolicy::No_referrer); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->SetHeaders(new InternalHeaders(HeadersGuardEnum::Request)); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr<Request> request = + new Request(global, std::move(internalRequest), nullptr); + + return FetchJSONStructure<IdentityRootManifest>(request)->Then( + GetCurrentSerialEventTarget(), __func__, + [aProvider](const IdentityRootManifest& manifest) { + // Make sure there is only one provider URL + if (manifest.mProvider_urls.Length() != 1) { + return IdentityCredential::ValidationPromise::CreateAndResolve( + false, __func__); + } + + // Resolve whether or not that provider URL is the one we were + // passed as an argument. + bool correctURL = manifest.mProvider_urls[0] == aProvider.mConfigURL; + return IdentityCredential::ValidationPromise::CreateAndResolve( + correctURL, __func__); + }, + [](nsresult error) { + return IdentityCredential::ValidationPromise::CreateAndReject(error, + __func__); + }); +} + +// static +RefPtr<IdentityCredential::GetManifestPromise> +IdentityCredential::FetchInternalManifest(nsIPrincipal* aPrincipal, + const IdentityProvider& aProvider) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Build the URL + nsCString configLocation = NS_ConvertUTF16toUTF8(aProvider.mConfigURL); + + // Create the global + RefPtr<NullPrincipal> nullPrincipal = + NullPrincipal::CreateWithInheritedAttributes(aPrincipal); + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr<nsIGlobalObject> global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> sandbox(cx); + nsresult rv = xpc->CreateSandbox(cx, nullPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetManifestPromise::CreateAndReject(rv, + __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::GetManifestPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr<InternalRequest>(configLocation, fragment); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Omit); + internalRequest->SetReferrerPolicy(ReferrerPolicy::No_referrer); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->SetHeaders(new InternalHeaders(HeadersGuardEnum::Request)); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr<Request> request = + new Request(global, std::move(internalRequest), nullptr); + return FetchJSONStructure<IdentityInternalManifest>(request); +} + +// static +RefPtr<IdentityCredential::GetAccountListPromise> +IdentityCredential::FetchAccountList( + nsIPrincipal* aPrincipal, const IdentityProvider& aProvider, + const IdentityInternalManifest& aManifest) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Build the URL + nsCOMPtr<nsIURI> baseURI; + nsCString baseURIString = NS_ConvertUTF16toUTF8(aProvider.mConfigURL); + nsresult rv = NS_NewURI(getter_AddRefs(baseURI), baseURIString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetAccountListPromise::CreateAndReject(rv, + __func__); + } + nsCOMPtr<nsIURI> idpURI; + nsCString accountSpec = NS_ConvertUTF16toUTF8(aManifest.mAccounts_endpoint); + rv = NS_NewURI(getter_AddRefs(idpURI), accountSpec.get(), baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetAccountListPromise::CreateAndReject(rv, + __func__); + } + nsCString configLocation; + rv = idpURI->GetSpec(configLocation); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetAccountListPromise::CreateAndReject(rv, + __func__); + } + + // Build the principal to use for this connection + // This is an expanded principal! It has the cookies of the IDP because it + // subsumes the constituent principals. It also has no serializable origin, + // so it won't send an Origin header even though this is a CORS mode + // request. It accomplishes this without being a SystemPrincipal too. + nsCOMPtr<nsIPrincipal> idpPrincipal = BasePrincipal::CreateContentPrincipal( + idpURI, aPrincipal->OriginAttributesRef()); + nsCOMPtr<nsIPrincipal> nullPrincipal = + NullPrincipal::CreateWithInheritedAttributes(aPrincipal); + AutoTArray<nsCOMPtr<nsIPrincipal>, 2> allowList = {idpPrincipal, + nullPrincipal}; + RefPtr<ExpandedPrincipal> expandedPrincipal = + ExpandedPrincipal::Create(allowList, aPrincipal->OriginAttributesRef()); + + // Create the global + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr<nsIGlobalObject> global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> sandbox(cx); + rv = xpc->CreateSandbox(cx, expandedPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetAccountListPromise::CreateAndReject(rv, + __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::GetAccountListPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr<InternalRequest>(configLocation, fragment); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Include); + internalRequest->SetReferrerPolicy(ReferrerPolicy::No_referrer); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->SetHeaders(new InternalHeaders(HeadersGuardEnum::Request)); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr<Request> request = + new Request(global, std::move(internalRequest), nullptr); + + return FetchJSONStructure<IdentityAccountList>(request)->Then( + GetCurrentSerialEventTarget(), __func__, + [aManifest](const IdentityAccountList& accountList) { + return IdentityCredential::GetAccountListPromise::CreateAndResolve( + MakeTuple(aManifest, accountList), __func__); + }, + [](nsresult error) { + return IdentityCredential::GetAccountListPromise::CreateAndReject( + error, __func__); + }); +} + +// static +RefPtr<IdentityCredential::GetTokenPromise> IdentityCredential::FetchToken( + nsIPrincipal* aPrincipal, const IdentityProvider& aProvider, + const IdentityInternalManifest& aManifest, + const IdentityAccount& aAccount) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Build the URL + nsCOMPtr<nsIURI> baseURI; + nsCString baseURIString = NS_ConvertUTF16toUTF8(aProvider.mConfigURL); + nsresult rv = NS_NewURI(getter_AddRefs(baseURI), baseURIString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetTokenPromise::CreateAndReject(rv, __func__); + } + nsCOMPtr<nsIURI> idpURI; + nsCString tokenSpec = NS_ConvertUTF16toUTF8(aManifest.mId_token_endpoint); + rv = NS_NewURI(getter_AddRefs(idpURI), tokenSpec.get(), baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetTokenPromise::CreateAndReject(rv, __func__); + } + nsCString tokenLocation; + rv = idpURI->GetSpec(tokenLocation); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetTokenPromise::CreateAndReject(rv, __func__); + } + + // Create the global + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr<nsIGlobalObject> global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> sandbox(cx); + rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetTokenPromise::CreateAndReject(rv, __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::GetTokenPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr<InternalRequest>(tokenLocation, fragment); + internalRequest->SetMethod("POST"_ns); + URLParams bodyValue; + bodyValue.Set(u"account_id"_ns, aAccount.mId); + bodyValue.Set(u"client_id"_ns, aProvider.mClientId); + if (aProvider.mNonce.WasPassed()) { + bodyValue.Set(u"nonce"_ns, aProvider.mNonce.Value()); + } + bodyValue.Set(u"disclosure_text_shown"_ns, u"false"_ns); + nsString bodyString; + bodyValue.Serialize(bodyString, true); + nsCString bodyCString = NS_ConvertUTF16toUTF8(bodyString); + nsCOMPtr<nsIInputStream> streamBody; + rv = NS_NewCStringInputStream(getter_AddRefs(streamBody), bodyCString); + if (NS_FAILED(rv)) { + return IdentityCredential::GetTokenPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + IgnoredErrorResult error; + RefPtr<InternalHeaders> internalHeaders = + new InternalHeaders(HeadersGuardEnum::Request); + internalHeaders->Set("Content-Type"_ns, + "application/x-www-form-urlencoded"_ns, error); + if (NS_WARN_IF(error.Failed())) { + return IdentityCredential::GetTokenPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + internalRequest->SetHeaders(internalHeaders); + internalRequest->SetBody(streamBody, bodyCString.Length()); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Include); + internalRequest->SetReferrerPolicy(ReferrerPolicy::Strict_origin); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr<Request> request = + new Request(global, std::move(internalRequest), nullptr); + return FetchJSONStructure<IdentityToken>(request)->Then( + GetCurrentSerialEventTarget(), __func__, + [aAccount](const IdentityToken& token) { + return IdentityCredential::GetTokenPromise::CreateAndResolve( + MakeTuple(token, aAccount), __func__); + }, + [](nsresult error) { + return IdentityCredential::GetTokenPromise::CreateAndReject(error, + __func__); + }); +} + +// static +RefPtr<IdentityCredential::GetMetadataPromise> +IdentityCredential::FetchMetadata(nsIPrincipal* aPrincipal, + const IdentityProvider& aProvider, + const IdentityInternalManifest& aManifest) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aPrincipal); + // Build the URL + nsCOMPtr<nsIURI> baseURI; + nsCString baseURIString = NS_ConvertUTF16toUTF8(aProvider.mConfigURL); + nsresult rv = NS_NewURI(getter_AddRefs(baseURI), baseURIString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetMetadataPromise::CreateAndReject(rv, + __func__); + } + nsCOMPtr<nsIURI> idpURI; + nsCString metadataSpec = + NS_ConvertUTF16toUTF8(aManifest.mClient_metadata_endpoint); + rv = NS_NewURI(getter_AddRefs(idpURI), metadataSpec.get(), baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetMetadataPromise::CreateAndReject(rv, + __func__); + } + nsCString configLocation; + rv = idpURI->GetSpec(configLocation); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetMetadataPromise::CreateAndReject(rv, + __func__); + } + + // Create the global + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + nsCOMPtr<nsIGlobalObject> global; + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> sandbox(cx); + rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IdentityCredential::GetMetadataPromise::CreateAndReject(rv, + __func__); + } + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + global = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!global)) { + return IdentityCredential::GetMetadataPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + + // Create a new request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr<InternalRequest>(configLocation, fragment); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Omit); + internalRequest->SetReferrerPolicy(ReferrerPolicy::No_referrer); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->SetHeaders(new InternalHeaders(HeadersGuardEnum::Request)); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr<Request> request = + new Request(global, std::move(internalRequest), nullptr); + return FetchJSONStructure<IdentityClientMetadata>(request); +} + +// static +RefPtr<IdentityCredential::GetIdentityProviderPromise> +IdentityCredential::PromptUserToSelectProvider( + BrowsingContext* aBrowsingContext, + const Sequence<IdentityProvider>& aProviders) { + MOZ_ASSERT(aBrowsingContext); + RefPtr<IdentityCredential::GetIdentityProviderPromise::Private> + resultPromise = + new IdentityCredential::GetIdentityProviderPromise::Private(__func__); + + if (NS_WARN_IF(!aBrowsingContext)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + nsresult error; + nsCOMPtr<nsIIdentityCredentialPromptService> icPromptService = + mozilla::components::IdentityCredentialPromptService::Service(&error); + if (NS_WARN_IF(!icPromptService)) { + resultPromise->Reject(error, __func__); + return resultPromise; + } + + nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(icPromptService); + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + JS::Rooted<JS::Value> providersJS(jsapi.cx()); + bool success = ToJSValue(jsapi.cx(), aProviders, &providersJS); + if (NS_WARN_IF(!success)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + RefPtr<Promise> showPromptPromise; + icPromptService->ShowProviderPrompt(aBrowsingContext, providersJS, + getter_AddRefs(showPromptPromise)); + + RefPtr<DomPromiseListener> listener = new DomPromiseListener( + [resultPromise](JSContext* aCx, JS::Handle<JS::Value> aValue) { + IdentityProvider result; + bool success = result.Init(aCx, aValue); + if (!success) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + resultPromise->Resolve(result, __func__); + }, + [resultPromise](nsresult aRv) { resultPromise->Reject(aRv, __func__); }); + showPromptPromise->AppendNativeHandler(listener); + + return resultPromise; +} + +// static +RefPtr<IdentityCredential::GetAccountPromise> +IdentityCredential::PromptUserToSelectAccount( + BrowsingContext* aBrowsingContext, const IdentityAccountList& aAccounts, + const IdentityInternalManifest& aManifest) { + MOZ_ASSERT(aBrowsingContext); + RefPtr<IdentityCredential::GetAccountPromise::Private> resultPromise = + new IdentityCredential::GetAccountPromise::Private(__func__); + + if (NS_WARN_IF(!aBrowsingContext)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + nsresult error; + nsCOMPtr<nsIIdentityCredentialPromptService> icPromptService = + mozilla::components::IdentityCredentialPromptService::Service(&error); + if (NS_WARN_IF(!icPromptService)) { + resultPromise->Reject(error, __func__); + return resultPromise; + } + + nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(icPromptService); + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + JS::Rooted<JS::Value> accountsJS(jsapi.cx()); + bool success = ToJSValue(jsapi.cx(), aAccounts, &accountsJS); + if (NS_WARN_IF(!success)) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + RefPtr<Promise> showPromptPromise; + icPromptService->ShowAccountListPrompt(aBrowsingContext, accountsJS, + getter_AddRefs(showPromptPromise)); + + RefPtr<DomPromiseListener> listener = new DomPromiseListener( + [resultPromise, aManifest](JSContext* aCx, JS::Handle<JS::Value> aValue) { + IdentityAccount result; + bool success = result.Init(aCx, aValue); + if (!success) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + resultPromise->Resolve(MakeTuple(aManifest, result), __func__); + }, + [resultPromise](nsresult aRv) { resultPromise->Reject(aRv, __func__); }); + showPromptPromise->AppendNativeHandler(listener); + + return resultPromise; +} + +// static +RefPtr<IdentityCredential::GetAccountPromise> +IdentityCredential::PromptUserWithPolicy( + BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal, + const IdentityAccount& aAccount, const IdentityInternalManifest& aManifest, + const IdentityProvider& aProvider) { + MOZ_ASSERT(aBrowsingContext); + MOZ_ASSERT(aPrincipal); + + nsresult error; + nsCOMPtr<nsIIdentityCredentialStorageService> icStorageService = + mozilla::components::IdentityCredentialStorageService::Service(&error); + if (NS_WARN_IF(!icStorageService)) { + return IdentityCredential::GetAccountPromise::CreateAndReject(error, + __func__); + } + + // Check the storage bit + nsCString configLocation = NS_ConvertUTF16toUTF8(aProvider.mConfigURL); + nsCOMPtr<nsIURI> idpURI; + error = NS_NewURI(getter_AddRefs(idpURI), configLocation); + if (NS_WARN_IF(NS_FAILED(error))) { + return IdentityCredential::GetAccountPromise::CreateAndReject(error, + __func__); + } + bool registered = false; + bool allowLogout = false; + nsCOMPtr<nsIPrincipal> idpPrincipal = BasePrincipal::CreateContentPrincipal( + idpURI, aPrincipal->OriginAttributesRef()); + error = icStorageService->GetState(aPrincipal, idpPrincipal, + NS_ConvertUTF16toUTF8(aAccount.mId), + ®istered, &allowLogout); + if (NS_WARN_IF(NS_FAILED(error))) { + return IdentityCredential::GetAccountPromise::CreateAndReject(error, + __func__); + } + + // if registered, mark as logged in and return + if (registered) { + icStorageService->SetState(aPrincipal, idpPrincipal, + NS_ConvertUTF16toUTF8(aAccount.mId), true, true); + return IdentityCredential::GetAccountPromise::CreateAndResolve( + MakeTuple(aManifest, aAccount), __func__); + } + + // otherwise, fetch ->Then display ->Then return ->Catch reject + RefPtr<BrowsingContext> browsingContext(aBrowsingContext); + nsCOMPtr<nsIPrincipal> argumentPrincipal(aPrincipal); + return FetchMetadata(aPrincipal, aProvider, aManifest) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aAccount, aProvider, argumentPrincipal, browsingContext, + icStorageService, + idpPrincipal](const IdentityClientMetadata& metadata) + -> RefPtr<GenericPromise> { + nsresult error; + nsCOMPtr<nsIIdentityCredentialPromptService> icPromptService = + mozilla::components::IdentityCredentialPromptService::Service( + &error); + if (NS_WARN_IF(!icPromptService)) { + return GenericPromise::CreateAndReject(error, __func__); + } + nsCOMPtr<nsIXPConnectWrappedJS> wrapped = + do_QueryInterface(icPromptService); + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(wrapped->GetJSObjectGlobal()))) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + JS::Rooted<JS::Value> providerJS(jsapi.cx()); + bool success = ToJSValue(jsapi.cx(), aProvider, &providerJS); + if (NS_WARN_IF(!success)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + JS::Rooted<JS::Value> metadataJS(jsapi.cx()); + success = ToJSValue(jsapi.cx(), metadata, &metadataJS); + if (NS_WARN_IF(!success)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + RefPtr<Promise> showPromptPromise; + icPromptService->ShowPolicyPrompt( + browsingContext, providerJS, metadataJS, + getter_AddRefs(showPromptPromise)); + + RefPtr<GenericPromise::Private> resultPromise = + new GenericPromise::Private(__func__); + RefPtr<DomPromiseListener> listener = new DomPromiseListener( + [aAccount, argumentPrincipal, idpPrincipal, resultPromise, + icStorageService](JSContext* aCx, + JS::Handle<JS::Value> aValue) { + bool isBool = aValue.isBoolean(); + if (!isBool) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + icStorageService->SetState( + argumentPrincipal, idpPrincipal, + NS_ConvertUTF16toUTF8(aAccount.mId), true, true); + resultPromise->Resolve(aValue.toBoolean(), __func__); + }, + [resultPromise](nsresult aRv) { + resultPromise->Reject(aRv, __func__); + }); + showPromptPromise->AppendNativeHandler(listener); + return resultPromise; + }, + [](nsresult error) { + return GenericPromise::CreateAndReject(error, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aManifest, aAccount](bool success) { + if (success) { + return IdentityCredential::GetAccountPromise::CreateAndResolve( + MakeTuple(aManifest, aAccount), __func__); + } + return IdentityCredential::GetAccountPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + }, + [](nsresult error) { + return IdentityCredential::GetAccountPromise::CreateAndReject( + error, __func__); + }); +} + +// static +void IdentityCredential::CloseUserInterface(BrowsingContext* aBrowsingContext) { + nsresult error; + nsCOMPtr<nsIIdentityCredentialPromptService> icPromptService = + mozilla::components::IdentityCredentialPromptService::Service(&error); + if (NS_WARN_IF(!icPromptService)) { + return; + } + icPromptService->Close(aBrowsingContext); +} + +// static +already_AddRefed<Promise> IdentityCredential::LogoutRPs( + GlobalObject& aGlobal, + const Sequence<IdentityCredentialLogoutRPsRequest>& aLogoutRequests, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Promise> promise = Promise::CreateResolvedWithUndefined(global, aRv); + NS_ENSURE_FALSE(aRv.Failed(), nullptr); + nsresult rv; + nsCOMPtr<nsIIdentityCredentialStorageService> icStorageService = + components::IdentityCredentialStorageService::Service(&rv); + if (NS_WARN_IF(!icStorageService)) { + aRv.Throw(rv); + return nullptr; + } + + RefPtr<nsIPrincipal> rpPrincipal = global->PrincipalOrNull(); + for (const auto& request : aLogoutRequests) { + // Get the current state + nsCOMPtr<nsIURI> idpURI; + rv = NS_NewURI(getter_AddRefs(idpURI), request.mUrl); + if (NS_FAILED(rv)) { + aRv.ThrowTypeError<MSG_INVALID_URL>(request.mUrl); + return nullptr; + } + nsCOMPtr<nsIPrincipal> idpPrincipal = BasePrincipal::CreateContentPrincipal( + idpURI, rpPrincipal->OriginAttributesRef()); + bool registered, allowLogout; + icStorageService->GetState(rpPrincipal, idpPrincipal, request.mAccountId, + ®istered, &allowLogout); + + // Ignore this request if it isn't permitted + if (!(registered && allowLogout)) { + continue; + } + + // Issue the logout request + constexpr auto fragment = ""_ns; + auto internalRequest = + MakeSafeRefPtr<InternalRequest>(request.mUrl, fragment); + internalRequest->SetRedirectMode(RequestRedirect::Error); + internalRequest->SetCredentialsMode(RequestCredentials::Include); + internalRequest->SetReferrerPolicy(ReferrerPolicy::Strict_origin); + internalRequest->SetMode(RequestMode::Cors); + internalRequest->SetCacheMode(RequestCache::No_cache); + internalRequest->OverrideContentPolicyType( + nsContentPolicyType::TYPE_WEB_IDENTITY); + RefPtr<Request> domRequest = + new Request(global, std::move(internalRequest), nullptr); + RequestOrUSVString fetchInput; + fetchInput.SetAsRequest() = domRequest; + RootedDictionary<RequestInit> requestInit(RootingCx()); + IgnoredErrorResult error; + RefPtr<Promise> fetchPromise = FetchRequest(global, fetchInput, requestInit, + CallerType::System, error); + + // Change state to disallow more logout requests + icStorageService->SetState(rpPrincipal, idpPrincipal, request.mAccountId, + true, false); + } + return promise.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/credentialmanagement/identity/IdentityCredential.h b/dom/credentialmanagement/identity/IdentityCredential.h new file mode 100644 index 0000000000..d7b4f6defb --- /dev/null +++ b/dom/credentialmanagement/identity/IdentityCredential.h @@ -0,0 +1,189 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_IdentityCredential_h +#define mozilla_dom_IdentityCredential_h + +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Credential.h" +#include "mozilla/dom/IPCIdentityCredential.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Tuple.h" + +namespace mozilla::dom { + +class IdentityCredential final : public Credential { + public: + typedef MozPromise<RefPtr<IdentityCredential>, nsresult, true> + GetIdentityCredentialPromise; + typedef MozPromise<IPCIdentityCredential, nsresult, true> + GetIPCIdentityCredentialPromise; + typedef MozPromise<IdentityProvider, nsresult, true> + GetIdentityProviderPromise; + typedef MozPromise<bool, nsresult, true> ValidationPromise; + typedef MozPromise<IdentityInternalManifest, nsresult, true> + GetManifestPromise; + typedef MozPromise<Tuple<IdentityInternalManifest, IdentityAccountList>, + nsresult, true> + GetAccountListPromise; + typedef MozPromise<Tuple<IdentityToken, IdentityAccount>, nsresult, true> + GetTokenPromise; + typedef MozPromise<Tuple<IdentityInternalManifest, IdentityAccount>, nsresult, + true> + GetAccountPromise; + typedef MozPromise<IdentityClientMetadata, nsresult, true> GetMetadataPromise; + + explicit IdentityCredential(nsPIDOMWindowInner* aParent); + + protected: + ~IdentityCredential() override; + + public: + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void CopyValuesFrom(const IPCIdentityCredential& aOther); + + IPCIdentityCredential MakeIPCIdentityCredential(); + + void GetToken(nsAString& aToken) const; + void SetToken(const nsAString& aToken); + + static already_AddRefed<Promise> LogoutRPs( + GlobalObject& aGlobal, + const Sequence<IdentityCredentialLogoutRPsRequest>& aLogoutRequests, + ErrorResult& aRv); + + static RefPtr<GetIdentityCredentialPromise> DiscoverFromExternalSource( + nsPIDOMWindowInner* aParent, const CredentialRequestOptions& aOptions, + bool aSameOriginWithAncestors); + + static RefPtr<GetIPCIdentityCredentialPromise> + DiscoverFromExternalSourceInMainProcess( + nsIPrincipal* aPrincipal, CanonicalBrowsingContext* aBrowsingContext, + const IdentityCredentialRequestOptions& aOptions); + + // Create an IPC credential that can be passed back to the content process. + // This calls a lot of helpers to do the logic of going from a single provider + // to a bearer token for an account at that provider. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to validate the root manifest of + // Return value: + // a promise resolving to an IPC credential with type "identity", id + // constructed to identify it, and token corresponding to the token + // fetched in FetchToken. This promise may reject with nsresult errors. + // Side effects: + // Will send network requests to the IDP. The details of which are in the + // other static methods here. + static RefPtr<GetIPCIdentityCredentialPromise> CreateCredential( + nsIPrincipal* aPrincipal, BrowsingContext* aBrowsingContext, + const IdentityProvider& aProvider); + + // Performs a Fetch for the root manifest of the provided identity provider + // and validates it as correct. The returned promise resolves with a bool + // that is true if everything is valid. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to validate the root manifest of + // Return value: + // promise that resolves to a bool that indicates success. Will reject + // when there are network or other errors. + // Side effects: + // Network request to the IDP's well-known from inside a NullPrincipal + // sandbox + // + static RefPtr<ValidationPromise> CheckRootManifest( + nsIPrincipal* aPrincipal, const IdentityProvider& aProvider); + + // Performs a Fetch for the internal manifest of the provided identity + // provider. The returned promise resolves with the manifest retrieved. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to fetch the root manifest + // Return value: + // promise that resolves to the internal manifest. Will reject + // when there are network or other errors. + // Side effects: + // Network request to the URL in aProvider as the manifest from inside a + // NullPrincipal sandbox + // + static RefPtr<GetManifestPromise> FetchInternalManifest( + nsIPrincipal* aPrincipal, const IdentityProvider& aProvider); + + // Performs a Fetch for the account list from the provided identity + // provider. The returned promise resolves with the manifest and the fetched + // account list in a tuple of objects. We put the argument manifest in the + // tuple to facilitate clean promise chaining. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to get account lists from + // aManifest: the provider's internal manifest + // Return value: + // promise that resolves to a Tuple of the passed manifest and the fetched + // account list. Will reject when there are network or other errors. + // Side effects: + // Network request to the provider supplied account endpoint with + // credentials but without any indication of aPrincipal. + // + static RefPtr<GetAccountListPromise> FetchAccountList( + nsIPrincipal* aPrincipal, const IdentityProvider& aProvider, + const IdentityInternalManifest& aManifest); + + // Performs a Fetch for a bearer token to the provided identity + // provider for a given account. The returned promise resolves with the + // account argument and the fetched token in a tuple of objects. + // We put the argument account in the + // tuple to facilitate clean promise chaining. + // + // Arguments: + // aPrincipal: the caller of navigator.credentials.get()'s principal + // aProvider: the provider to get account lists from + // aManifest: the provider's internal manifest + // aAccount: the account to request + // Return value: + // promise that resolves to a Tuple of the passed account and the fetched + // token. Will reject when there are network or other errors. + // Side effects: + // Network request to the provider supplied token endpoint with + // credentials and including information about the requesting principal. + // + static RefPtr<GetTokenPromise> FetchToken( + nsIPrincipal* aPrincipal, const IdentityProvider& aProvider, + const IdentityInternalManifest& aManifest, + const IdentityAccount& aAccount); + + static RefPtr<GetMetadataPromise> FetchMetadata( + nsIPrincipal* aPrincipal, const IdentityProvider& aProvider, + const IdentityInternalManifest& aManifest); + + static RefPtr<GetIdentityProviderPromise> PromptUserToSelectProvider( + BrowsingContext* aBrowsingContext, + const Sequence<IdentityProvider>& aProviders); + + static RefPtr<GetAccountPromise> PromptUserToSelectAccount( + BrowsingContext* aBrowsingContext, const IdentityAccountList& aAccounts, + const IdentityInternalManifest& aManifest); + + static RefPtr<GetAccountPromise> PromptUserWithPolicy( + BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal, + const IdentityAccount& aAccount, + const IdentityInternalManifest& aManifest, + const IdentityProvider& aProvider); + + static void CloseUserInterface(BrowsingContext* aBrowsingContext); + + private: + nsAutoString mToken; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_IdentityCredential_h diff --git a/dom/credentialmanagement/identity/IdentityCredentialSerializationHelpers.h b/dom/credentialmanagement/identity/IdentityCredentialSerializationHelpers.h new file mode 100644 index 0000000000..f9fae3a758 --- /dev/null +++ b/dom/credentialmanagement/identity/IdentityCredentialSerializationHelpers.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_identitycredentialserializationhelpers_h__ +#define mozilla_dom_identitycredentialserializationhelpers_h__ + +#include "mozilla/dom/IdentityCredential.h" +#include "mozilla/dom/IdentityCredentialBinding.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::dom::IdentityProvider> { + typedef mozilla::dom::IdentityProvider paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mConfigURL); + WriteParam(aWriter, aParam.mClientId); + WriteParam(aWriter, aParam.mNonce); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mConfigURL) && + ReadParam(aReader, &aResult->mClientId) && + ReadParam(aReader, &aResult->mNonce); + } +}; + +template <> +struct ParamTraits<mozilla::dom::IdentityCredentialRequestOptions> { + typedef mozilla::dom::IdentityCredentialRequestOptions paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mProviders); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return ReadParam(aReader, &aResult->mProviders); + ; + } +}; + +} // namespace IPC + +#endif // mozilla_dom_identitycredentialserializationhelpers_h__ diff --git a/dom/credentialmanagement/identity/IdentityNetworkHelpers.h b/dom/credentialmanagement/identity/IdentityNetworkHelpers.h new file mode 100644 index 0000000000..56cffb284f --- /dev/null +++ b/dom/credentialmanagement/identity/IdentityNetworkHelpers.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_IdentityNetworkHelpers_h +#define mozilla_dom_IdentityNetworkHelpers_h + +#include "mozilla/dom/Request.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/MozPromise.h" + +namespace mozilla::dom { + +template <typename T, typename TPromise = MozPromise<T, nsresult, true>> +RefPtr<TPromise> FetchJSONStructure(Request* aRequest) { + MOZ_ASSERT(XRE_IsParentProcess()); + + // Create the returned Promise + RefPtr<typename TPromise::Private> resultPromise = + new typename TPromise::Private(__func__); + + // Fetch the provided request + RequestOrUSVString fetchInput; + fetchInput.SetAsRequest() = aRequest; + RootedDictionary<RequestInit> requestInit(RootingCx()); + IgnoredErrorResult error; + RefPtr<Promise> fetchPromise = + FetchRequest(aRequest->GetParentObject(), fetchInput, requestInit, + CallerType::System, error); + if (NS_WARN_IF(error.Failed())) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return resultPromise; + } + + // Handle the response + RefPtr<DomPromiseListener> listener = new DomPromiseListener( + [resultPromise](JSContext* aCx, JS::Handle<JS::Value> aValue) { + // Get the Response object from the argument to the callback + if (NS_WARN_IF(!aValue.isObject())) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + MOZ_ASSERT(obj); + Response* response = nullptr; + if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Response, &obj, response)))) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + // Make sure the request was a success + if (!response->Ok()) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + // Parse the body into JSON, which must be done async + IgnoredErrorResult error; + RefPtr<Promise> jsonPromise = response->ConsumeBody( + aCx, BodyConsumer::ConsumeType::CONSUME_JSON, error); + if (NS_WARN_IF(error.Failed())) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + // Handle the parsed JSON from the Response body + RefPtr<DomPromiseListener> jsonListener = new DomPromiseListener( + [resultPromise](JSContext* aCx, JS::Handle<JS::Value> aValue) { + // Parse the JSON into the correct type, validating fields and + // types + T result; + bool success = result.Init(aCx, aValue); + if (!success) { + resultPromise->Reject(NS_ERROR_FAILURE, __func__); + return; + } + + resultPromise->Resolve(result, __func__); + }, + [resultPromise](nsresult aRv) { + resultPromise->Reject(aRv, __func__); + }); + jsonPromise->AppendNativeHandler(jsonListener); + }, + [resultPromise](nsresult aRv) { resultPromise->Reject(aRv, __func__); }); + fetchPromise->AppendNativeHandler(listener); + return resultPromise; +} + +} // namespace mozilla::dom + +#endif // mozilla_dom_IdentityNetworkHelpers_h diff --git a/dom/credentialmanagement/identity/moz.build b/dom/credentialmanagement/identity/moz.build new file mode 100644 index 0000000000..240a138bb6 --- /dev/null +++ b/dom/credentialmanagement/identity/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Credential Management") + +EXPORTS.mozilla.dom += [ + "IdentityCredential.h", + "IdentityCredentialSerializationHelpers.h", + "IdentityNetworkHelpers.h", +] + +IPDL_SOURCES += [ + "IPCIdentityCredential.ipdlh", +] + +UNIFIED_SOURCES += ["IdentityCredential.cpp"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.ini"] diff --git a/dom/credentialmanagement/identity/tests/mochitest/head.js b/dom/credentialmanagement/identity/tests/mochitest/head.js new file mode 100644 index 0000000000..393ba9fa23 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/head.js @@ -0,0 +1,24 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var idp_host = "https://example.net"; +var test_path = "/tests/dom/credentialmanagement/identity/tests/mochitest"; +var idp_api = idp_host + test_path; + +async function setupTest(testName) { + ok( + window.location.pathname.includes(testName), + `Must set the right test name when setting up. Test name "${testName}" must be in URL path "${window.location.pathname}"` + ); + let fetchPromise = fetch( + `${idp_api}/server_manifest.sjs?set_test=${testName}` + ); + let focusPromise = SimpleTest.promiseFocus(); + window.open(`${idp_api}/helper_set_cookie.html`, "_blank"); + await focusPromise; + return fetchPromise; +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html b/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html new file mode 100644 index 0000000000..9f8e410b90 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> + window.close(); +</script> + +See ya! (This window will close itself) +The cookie was set via HTTP. diff --git a/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html^headers^ b/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html^headers^ new file mode 100644 index 0000000000..c221facafc --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/helper_set_cookie.html^headers^ @@ -0,0 +1 @@ +Set-Cookie: credential=authcookieval; SameSite=None; Secure; Path=/ diff --git a/dom/credentialmanagement/identity/tests/mochitest/mochitest.ini b/dom/credentialmanagement/identity/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..4106cfb55b --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/mochitest.ini @@ -0,0 +1,48 @@ +[DEFAULT] +prefs = + dom.security.credentialmanagement.identity.enabled=true + dom.security.credentialmanagement.identity.select_first_in_ui_lists=true + dom.security.credentialmanagement.identity.reject_delay.enabled=false + privacy.antitracking.enableWebcompat=false # disables opener heuristic +scheme = https +skip-if = xorigin + +support-files = + head.js + helper_set_cookie.html + helper_set_cookie.html^headers^ + /.well-known/web-identity + /.well-known/web-identity^headers^ + server_manifest.sjs + server_manifest_wrong_provider_in_manifest.sjs + server_metadata.json + server_metadata.json^headers^ + server_simple_accounts.sjs + server_simple_idtoken.sjs + server_no_accounts_accounts.sjs + server_no_accounts_idtoken.sjs + server_two_accounts_accounts.sjs + server_two_accounts_idtoken.sjs + server_two_providers_accounts.sjs + server_two_providers_idtoken.sjs + server_accounts_error_accounts.sjs + server_accounts_error_idtoken.sjs + server_idtoken_error_accounts.sjs + server_idtoken_error_idtoken.sjs + server_accounts_redirect_accounts.sjs + server_accounts_redirect_idtoken.sjs + server_idtoken_redirect_accounts.sjs + server_idtoken_redirect_idtoken.sjs + +[test_simple.html] +[test_no_accounts.html] +[test_two_accounts.html] +[test_accounts_error.html] +[test_idtoken_error.html] +[test_accounts_redirect.html] +[test_idtoken_redirect.html] +[test_wrong_provider_in_manifest.html] +[test_get_without_providers.html] +[test_empty_provider_list.html] +[test_two_providers.html] +[test_delay_reject.html] diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_accounts.sjs new file mode 100644 index 0000000000..d0a11ce469 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_accounts.sjs @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setStatusLine(request.httpVersion, 503, "Service Unavailable"); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_idtoken.sjs new file mode 100644 index 0000000000..a6f6f7c4b1 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_error_idtoken.sjs @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let responseContent = { + token: "should not be returned", + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_accounts.sjs new file mode 100644 index 0000000000..f33da643a0 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_accounts.sjs @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Location", "server_simple_accounts.sjs"); + response.setStatusLine(request.httpVersion, 302, "Found"); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_idtoken.sjs new file mode 100644 index 0000000000..a6f6f7c4b1 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_accounts_redirect_idtoken.sjs @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let responseContent = { + token: "should not be returned", + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_accounts.sjs new file mode 100644 index 0000000000..9baeb51ba5 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_accounts.sjs @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_idtoken.sjs new file mode 100644 index 0000000000..653207672b --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_error_idtoken.sjs @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setStatusLine(request.httpVersion, 503, "Service Unavailable"); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_accounts.sjs new file mode 100644 index 0000000000..9baeb51ba5 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_accounts.sjs @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_idtoken.sjs new file mode 100644 index 0000000000..66456eb687 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_idtoken_redirect_idtoken.sjs @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Location", "server_simple_idtoken.sjs"); + response.setStatusLine(request.httpVersion, 302, "Found"); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs new file mode 100644 index 0000000000..afd575e21a --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + let params = new URLSearchParams(request.queryString); + let test = params.get("set_test"); + if (test === null) { + test = getState("test"); + } else { + setState("test", test); + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setStatusLine(request.httpVersion, 200, "OK"); + return; + } + + if (request.hasHeader("Cookie")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_TESTNAME_accounts.sjs", + client_metadata_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json", + id_token_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_TESTNAME_idtoken.sjs", + }; + let bodyFormat = JSON.stringify(content); + let body = bodyFormat.replaceAll("TESTNAME", test); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs new file mode 100644 index 0000000000..ec8730ee13 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs", + client_metadata_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_simple_metadata.sjs", + id_token_endpoint: + "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs", + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json b/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json new file mode 100644 index 0000000000..6616407523 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json @@ -0,0 +1,4 @@ +{ + "privacy_policy_url" : "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/null.txt", + "terms_of_service_url" : "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/null.txt" +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json^headers^ b/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json^headers^ new file mode 100644 index 0000000000..75875a7cf3 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_metadata.json^headers^ @@ -0,0 +1,2 @@ +Content-Type: application/json +Access-Control-Allow-Origin: * diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_accounts.sjs new file mode 100644 index 0000000000..dac20e4466 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_accounts.sjs @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_idtoken.sjs new file mode 100644 index 0000000000..a3ca4ce31d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_no_accounts_idtoken.sjs @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Referer") || + request.getHeader("Referer") != "https://example.com/" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Origin") || + request.getHeader("Origin") != "https://example.com" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let requestContent = readStream( + new BinaryInputStream(request.bodyInputStream) + ); + let responseContent = { + token: requestContent, + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs new file mode 100644 index 0000000000..6ebce36802 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_simple_accounts.sjs @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs new file mode 100644 index 0000000000..a3ca4ce31d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_simple_idtoken.sjs @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Referer") || + request.getHeader("Referer") != "https://example.com/" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Origin") || + request.getHeader("Origin") != "https://example.com" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let requestContent = readStream( + new BinaryInputStream(request.bodyInputStream) + ); + let responseContent = { + token: requestContent, + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_accounts.sjs new file mode 100644 index 0000000000..f9d60183a1 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_accounts.sjs @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + { + id: "5678", + given_name: "Johnny", + name: "Johnny", + email: "johnny@idp.example", + picture: "https://idp.example/profile/456", + approved_clients: ["abc", "def", "ghi"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_idtoken.sjs new file mode 100644 index 0000000000..a3ca4ce31d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_two_accounts_idtoken.sjs @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Sec-Fetch-Dest") || + request.getHeader("Sec-Fetch-Dest") != "webidentity" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Referer") || + request.getHeader("Referer") != "https://example.com/" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Origin") || + request.getHeader("Origin") != "https://example.com" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let requestContent = readStream( + new BinaryInputStream(request.bodyInputStream) + ); + let responseContent = { + token: requestContent, + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_accounts.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_accounts.sjs new file mode 100644 index 0000000000..25060b850e --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_accounts.sjs @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(request, response) { + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Origin") && request.getHeader("Origin") != "null") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if (request.hasHeader("Referer")) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let content = { + accounts: [ + { + id: "1234", + given_name: "John", + name: "John Doe", + email: "john_doe@idp.example", + picture: "https://idp.example/profile/123", + approved_clients: ["123", "456", "789"], + }, + { + id: "5678", + given_name: "Johnny", + name: "Johnny", + email: "johnny@idp.example", + picture: "https://idp.example/profile/456", + approved_clients: ["abc", "def", "ghi"], + }, + ], + }; + let body = JSON.stringify(content); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_idtoken.sjs b/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_idtoken.sjs new file mode 100644 index 0000000000..01b61ff33d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/server_two_providers_idtoken.sjs @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function readStream(inputStream) { + let available = 0; + let result = []; + while ((available = inputStream.available()) > 0) { + result.push(inputStream.readBytes(available)); + } + return result.join(""); +} + +function handleRequest(request, response) { + if (request.method != "POST") { + response.setStatusLine(request.httpVersion, 405, "Method Not Allowed"); + return; + } + if ( + !request.hasHeader("Cookie") || + request.getHeader("Cookie") != "credential=authcookieval" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Referer") || + request.getHeader("Referer") != "https://example.com/" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + if ( + !request.hasHeader("Origin") || + request.getHeader("Origin") != "https://example.com" + ) { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + return; + } + + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Content-Type", "application/json"); + let requestContent = readStream( + new BinaryInputStream(request.bodyInputStream) + ); + let responseContent = { + token: requestContent, + }; + let body = JSON.stringify(responseContent); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(body); +} diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_accounts_error.html b/dom/credentialmanagement/identity/tests/mochitest/test_accounts_error.html new file mode 100644 index 0000000000..ca0f85b110 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_accounts_error.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Server Error On Accounts Endpoint</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("accounts_error").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }] + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test verifies that we do not get a credential when the accounts endpoint returns an error.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_accounts_redirect.html b/dom/credentialmanagement/identity/tests/mochitest/test_accounts_redirect.html new file mode 100644 index 0000000000..99b897d35e --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_accounts_redirect.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Server Redirect On Accounts Endpoint</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("accounts_redirect").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }] + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test verifies that we do not get a credential when the accounts endpoint redirects to another (entirely functional) accounts endpoint.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_delay_reject.html b/dom/credentialmanagement/identity/tests/mochitest/test_delay_reject.html new file mode 100644 index 0000000000..0151f4b6c4 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_delay_reject.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Delay Reject</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.credentialmanagement.identity.reject_delay.enabled", "true" ], + ["dom.security.credentialmanagement.identity.reject_delay.duration_ms", "1000" ], + ] }) + .then(() => {setupTest("delay_reject")}) + .then( + function () { + return navigator.credentials.get({ + identity: { + providers: [] + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test verifies that our rejections are delayed, checking for >500ms.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_empty_provider_list.html b/dom/credentialmanagement/identity/tests/mochitest/test_empty_provider_list.html new file mode 100644 index 0000000000..ad5b6ea28c --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_empty_provider_list.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Empty Provider List</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("empty_provider_list") + .then( + function () { + return navigator.credentials.get({ + identity: { + providers: [] + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test verifies that we do not get a credential when we give no providers to support.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_get_without_providers.html b/dom/credentialmanagement/identity/tests/mochitest/test_get_without_providers.html new file mode 100644 index 0000000000..4425abf5aa --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_get_without_providers.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>No Providers Specified</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("get_without_providers").then( + function () { + return navigator.credentials.get({ + identity: { + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test verifies that we do not get a credential when we give no providers field in the JSON. This is mostly to make sure we don't have any nullptr derefs.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_error.html b/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_error.html new file mode 100644 index 0000000000..ddc6716081 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_error.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Server Error On Token Endpoint</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("idtoken_error").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }] + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test verifies that we do not get a credential when the idtoken endpoint returns an error.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_redirect.html b/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_redirect.html new file mode 100644 index 0000000000..88512a1d22 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_idtoken_redirect.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Server Redirect On Token Endpoint</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("idtoken_redirect").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }] + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test verifies that we do not get a credential when the idtoken endpoint redirects to another (entirely functional) idtoken endpoint.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_no_accounts.html b/dom/credentialmanagement/identity/tests/mochitest/test_no_accounts.html new file mode 100644 index 0000000000..90c3335eda --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_no_accounts.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>No Accounts in the List</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("no_accounts").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }] + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test validates that if a provider returns no accounts, we throw an error from our credential request.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_simple.html b/dom/credentialmanagement/identity/tests/mochitest/test_simple.html new file mode 100644 index 0000000000..39d34f3d5f --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_simple.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Happypath Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("simple").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }] + } + }); + } + ).then((cred) => { + ok(true, "successfully got a credential"); + is(cred.token, + "account_id=1234&client_id=mochitest&nonce=nonce&disclosure_text_shown=false", + "Correct token on the credential."); + is(cred.id, + "1234", + "Correct id on the credential"); + is(cred.type, + "identity", + "Correct type on the credential"); + }).catch((err) => { + ok(false, "must not have an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This is the main happypath test. We get a credential in a way that should work. This includes simplifying some logic like exactly one account and provider.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_two_accounts.html b/dom/credentialmanagement/identity/tests/mochitest/test_two_accounts.html new file mode 100644 index 0000000000..36e99adf75 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_two_accounts.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Two Accounts in the List</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("two_accounts").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }] + } + }); + } + ).then((cred) => { + ok(true, "successfully got a credential"); + is(cred.token, + "account_id=1234&client_id=mochitest&nonce=nonce&disclosure_text_shown=false", + "Correct token on the credential."); + is(cred.id, + "1234", + "Correct id on the credential"); + is(cred.type, + "identity", + "Correct type on the credential"); + }).catch((err) => { + ok(false, "must not have an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test is temporary until we have an account chooser. It verifies that when we get more than one account from the IDP we just pick the first one.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_two_providers.html b/dom/credentialmanagement/identity/tests/mochitest/test_two_providers.html new file mode 100644 index 0000000000..5533f71064 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_two_providers.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Two Providers in a Credential.get()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("two_providers").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }, + { + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs", + clientId: "mochitest", + nonce: "nonce2" + } + ] + } + }); + } + ).then((cred) => { + ok(true, "successfully got a credential"); + is(cred.token, + "account_id=1234&client_id=mochitest&nonce=nonce&disclosure_text_shown=false", + "Correct token on the credential."); + is(cred.id, + "1234", + "Correct id on the credential"); + is(cred.type, + "identity", + "Correct type on the credential"); + }).catch((err) => { + ok(false, "must not have an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/test_wrong_provider_in_manifest.html b/dom/credentialmanagement/identity/tests/mochitest/test_wrong_provider_in_manifest.html new file mode 100644 index 0000000000..8ff1afe04d --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/test_wrong_provider_in_manifest.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Manifest Disagreement</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + setupTest("wrong_provider_in_manifest").then( + function () { + return navigator.credentials.get({ + identity: { + providers: [{ + configURL: "https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest_wrong_provider_in_manifest.sjs", + clientId: "mochitest", + nonce: "nonce" + }] + } + }); + } + ).then((cred) => { + ok(false, "incorrectly got a credential"); + }).catch((err) => { + ok(true, "correctly got an error"); + }).finally(() => { + SimpleTest.finish(); + }) + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none">This test is an important privacy check. We make sure the manifest from the argument to credentials.get matches the IDP's root manifest.</div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/credentialmanagement/identity/tests/mochitest/web-identity b/dom/credentialmanagement/identity/tests/mochitest/web-identity new file mode 100644 index 0000000000..33dc9c455b --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/web-identity @@ -0,0 +1 @@ +{"provider_urls": ["https://example.net/tests/dom/credentialmanagement/identity/tests/mochitest/server_manifest.sjs"]} diff --git a/dom/credentialmanagement/identity/tests/mochitest/web-identity^headers^ b/dom/credentialmanagement/identity/tests/mochitest/web-identity^headers^ new file mode 100644 index 0000000000..75875a7cf3 --- /dev/null +++ b/dom/credentialmanagement/identity/tests/mochitest/web-identity^headers^ @@ -0,0 +1,2 @@ +Content-Type: application/json +Access-Control-Allow-Origin: * |