From 2e736bd3a688723d0b9d765f85c3091bc0807c91 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Wed, 12 Apr 2023 12:23:27 -0600 Subject: [PATCH 1/4] fix: redirect to oauth success url --- src/webOAuthServer.ts | 4 +++- test/unit/webOauthServerTest.ts | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/webOAuthServer.ts b/src/webOAuthServer.ts index 7a5c8178fb..505aebd232 100644 --- a/src/webOAuthServer.ts +++ b/src/webOAuthServer.ts @@ -94,7 +94,9 @@ export class WebOAuthServer extends AsyncCreatable { oauth2: this.oauth2, }); await authInfo.save(); - this.webServer.doRedirect(303, authInfo.getOrgFrontDoorUrl(), response); + const loginUrl = this.oauth2.loginUrl.replace(/\/+$/, ''); + const oauthSuccessUrl = `${loginUrl}/services/oauth2/success`; + this.webServer.doRedirect(303, oauthSuccessUrl, response); response.end(); resolve(authInfo); } catch (err) { diff --git a/test/unit/webOauthServerTest.ts b/test/unit/webOauthServerTest.ts index a153e466db..84b1f5288e 100644 --- a/test/unit/webOauthServerTest.ts +++ b/test/unit/webOauthServerTest.ts @@ -43,7 +43,7 @@ describe('WebOauthServer', () => { describe('authorizeAndSave', () => { const testData = new MockTestOrgData(); - const frontDoorUrl = 'https://www.frontdoor.com'; + const oauthSuccessUrl = 'https://login.salesforce.com/services/oauth2/success'; let authFields: AuthFields; let authInfoStub: StubbedType; let serverResponseStub: StubbedType; @@ -54,7 +54,6 @@ describe('WebOauthServer', () => { authFields = await testData.getConfig(); authInfoStub = stubInterface($$.SANDBOX, { getFields: () => authFields, - getOrgFrontDoorUrl: () => frontDoorUrl, }); serverResponseStub = stubInterface($$.SANDBOX, {}); @@ -71,12 +70,12 @@ describe('WebOauthServer', () => { expect(authInfo.getFields()).to.deep.equal(authFields); }); - it('should redirect to front door url', async () => { + it('should redirect to oauth success url', async () => { const oauthServer = await WebOAuthServer.create({ oauthConfig: {} }); await oauthServer.start(); await oauthServer.authorizeAndSave(); expect(redirectStub.callCount).to.equal(1); - expect(redirectStub.args).to.deep.equal([[303, frontDoorUrl, serverResponseStub]]); + expect(redirectStub.args).to.deep.equal([[303, oauthSuccessUrl, serverResponseStub]]); }); it('should report error', async () => { From 209a03f84063405a2eba682304b2685fee8fa066 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Fri, 21 Apr 2023 09:44:37 -0600 Subject: [PATCH 2/4] feat: handle success and error redirects from web oauth --- messages/auth.md | 10 ++- src/org/authInfo.ts | 2 +- src/webOAuthServer.ts | 75 ++++++++++++++--- test/unit/webOauthServerTest.ts | 138 +++++++++++++++++++++++++++++--- 4 files changed, 198 insertions(+), 27 deletions(-) diff --git a/messages/auth.md b/messages/auth.md index 6076e125aa..3e390daadd 100644 --- a/messages/auth.md +++ b/messages/auth.md @@ -30,8 +30,16 @@ The device authorization request timed out. After executing force:auth:device:lo # serverErrorHTMLResponse -

%s


This is most likely not an error with the Salesforce CLI. Please ensure all information is accurate and try again. +
%s

This is most likely not an error with the Salesforce CLI. Please ensure all information is accurate and try again.
# missingAuthCode No authentication code found on login response. + +# serverSuccessHTMLResponse + +
You've successfully logged in. You can now close this browser tab or window.
+ +# serverSfdcImage + +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIxLjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyNjIgMTg0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyNjIgMTg0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6IzAwQTFFMDt9Cgkuc3Qxe2ZpbGw6I0ZGRkZGRjt9Cjwvc3R5bGU+Cjx0aXRsZT5sb2dvLXNhbGVzZm9yY2U8L3RpdGxlPgo8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KPGcgaWQ9IlRlc3QtQiI+Cgk8ZyBpZD0iTW9iaWxlLU5hdi0tLVRlc3QtQi1feDI4XzBfeDI5XyI+CgkJPGcgaWQ9Ikdyb3VwIj4KCQkJPGcgaWQ9ImxvZ28tc2FsZXNmb3JjZSI+CgkJCQk8cGF0aCBpZD0iRmlsbC0xIiBjbGFzcz0ic3QwIiBkPSJNMTA5LjIsMjAuOWM4LjQtOC43LDIwLjEtMTQuMiwzMy0xNC4yYzE3LjIsMCwzMi4xLDkuNiw0MC4xLDIzLjhjNi45LTMuMSwxNC42LTQuOCwyMi43LTQuOAoJCQkJCWMzMSwwLDU2LDI1LjMsNTYsNTYuNXMtMjUuMSw1Ni41LTU2LDU2LjVjLTMuOCwwLTcuNS0wLjQtMTEtMS4xYy03LDEyLjUtMjAuNCwyMS0zNS44LDIxYy02LjQsMC0xMi41LTEuNS0xNy45LTQuMQoJCQkJCWMtNy4xLDE2LjctMjMuNywyOC41LTQzLDI4LjVjLTIwLjEsMC0zNy4zLTEyLjctNDMuOS0zMC42Yy0yLjksMC42LTUuOSwwLjktOC45LDAuOWMtMjQsMC00My40LTE5LjYtNDMuNC00My45CgkJCQkJYzAtMTYuMiw4LjctMzAuNCwyMS43LTM4Yy0yLjctNi4xLTQuMi0xMi45LTQuMi0yMC4xQzE4LjUsMjMuNiw0MS4yLDEsNjksMUM4NS40LDEsMTAwLDguOCwxMDkuMiwyMC45Ii8+CgkJCQk8cGF0aCBpZD0iQ29tYmluZWQtU2hhcGUiIGNsYXNzPSJzdDEiIGQ9Ik0zOC43LDk1LjRsMS4xLTIuOWMwLjItMC41LDAuNS0wLjMsMC43LTAuMmMwLjMsMC4yLDAuNSwwLjMsMC45LDAuNmMzLjEsMiw2LDIsNi45LDIKCQkJCQljMi4zLDAsMy44LTEuMiwzLjgtMi45di0wLjFjMC0xLjgtMi4yLTIuNS00LjgtMy4zbC0wLjYtMC4yYy0zLjUtMS03LjMtMi41LTcuMy02Ljl2LTAuMWMwLTQuMiwzLjQtNy4yLDguMy03LjJsMC41LDAKCQkJCQljMi45LDAsNS42LDAuOCw3LjYsMi4xYzAuMiwwLjEsMC40LDAuMywwLjMsMC42Yy0wLjEsMC4zLTEsMi42LTEuMSwyLjljLTAuMiwwLjUtMC43LDAuMi0wLjcsMC4yYy0xLjgtMS00LjUtMS43LTYuOC0xLjcKCQkJCQljLTIuMSwwLTMuNCwxLjEtMy40LDIuNnYwLjFjMCwxLjcsMi4zLDIuNSw0LjksMy4zbDAuNSwwLjFjMy41LDEuMSw3LjIsMi42LDcuMiw2Ljl2MC4xYzAsNC42LTMuMyw3LjQtOC42LDcuNAoJCQkJCWMtMi42LDAtNS4xLTAuNC03LjgtMS44Yy0wLjUtMC4zLTEtMC41LTEuNS0wLjlDMzguNyw5NS45LDM4LjUsOTUuOCwzOC43LDk1LjR6IE0xMTYuNyw5NS40bDEuMS0yLjljMC4yLTAuNSwwLjYtMC4zLDAuNy0wLjIKCQkJCQljMC4zLDAuMiwwLjUsMC4zLDAuOSwwLjZjMy4xLDIsNiwyLDYuOSwyYzIuMywwLDMuOC0xLjIsMy44LTIuOXYtMC4xYzAtMS44LTIuMi0yLjUtNC44LTMuM2wtMC42LTAuMmMtMy41LTEtNy4zLTIuNS03LjMtNi45CgkJCQkJdi0wLjFjMC00LjIsMy40LTcuMiw4LjMtNy4ybDAuNSwwYzIuOSwwLDUuNiwwLjgsNy42LDIuMWMwLjIsMC4xLDAuNCwwLjMsMC4zLDAuNmMtMC4xLDAuMy0xLDIuNi0xLjEsMi45CgkJCQkJYy0wLjIsMC41LTAuNywwLjItMC43LDAuMmMtMS44LTEtNC41LTEuNy02LjgtMS43Yy0yLjEsMC0zLjQsMS4xLTMuNCwyLjZ2MC4xYzAsMS43LDIuMywyLjUsNC45LDMuM2wwLjUsMC4xCgkJCQkJYzMuNSwxLjEsNy4yLDIuNiw3LjIsNi45djAuMWMwLDQuNi0zLjMsNy40LTguNiw3LjRjLTIuNiwwLTUuMS0wLjQtNy44LTEuOGMtMC41LTAuMy0xLTAuNS0xLjUtMC45CgkJCQkJQzExNi44LDk1LjksMTE2LjYsOTUuOCwxMTYuNyw5NS40eiBNMTc0LjUsODEuN2MwLjQsMS41LDAuNywzLjEsMC43LDQuOHMtMC4yLDMuMy0wLjcsNC44Yy0wLjQsMS41LTEuMSwyLjgtMiwzLjkKCQkJCQljLTAuOSwxLjEtMi4xLDItMy40LDIuNmMtMS40LDAuNi0zLDAuOS00LjgsMC45Yy0xLjgsMC0zLjQtMC4zLTQuOC0wLjljLTEuNC0wLjYtMi41LTEuNS0zLjQtMi42Yy0wLjktMS4xLTEuNi0yLjQtMi0zLjkKCQkJCQljLTAuNC0xLjUtMC43LTMuMS0wLjctNC44YzAtMS43LDAuMi0zLjMsMC43LTQuOGMwLjQtMS41LDEuMS0yLjgsMi0zLjljMC45LTEuMSwyLjEtMiwzLjQtMi42YzEuNC0wLjYsMy0xLDQuOC0xCgkJCQkJYzEuOCwwLDMuNCwwLjMsNC44LDFjMS40LDAuNiwyLjUsMS41LDMuNCwyLjZDMTczLjQsNzguOSwxNzQuMSw4MC4yLDE3NC41LDgxLjd6IE0xNzAsODYuNGMwLTIuNi0wLjUtNC42LTEuNC02CgkJCQkJYy0wLjktMS40LTIuNC0yLjEtNC4zLTIuMWMtMiwwLTMuNCwwLjctNC4zLDIuMWMtMC45LDEuNC0xLjQsMy40LTEuNCw2YzAsMi42LDAuNSw0LjYsMS40LDYuMWMwLjksMS40LDIuMywyLjEsNC4zLDIuMQoJCQkJCWMyLDAsMy40LTAuNyw0LjMtMi4xQzE2OS42LDkxLjEsMTcwLDg5LDE3MCw4Ni40eiBNMjExLjEsOTMuOWwxLjEsM2MwLjEsMC40LTAuMiwwLjUtMC4yLDAuNWMtMS43LDAuNy00LDEuMS02LjMsMS4xCgkJCQkJYy0zLjksMC02LjgtMS4xLTguOC0zLjNjLTItMi4yLTMtNS4yLTMtOC45YzAtMS43LDAuMi0zLjMsMC43LTQuOGMwLjUtMS41LDEuMi0yLjgsMi4yLTMuOWMxLTEuMSwyLjItMiwzLjYtMi42CgkJCQkJYzEuNC0wLjYsMy4xLTEsNS0xYzEuMywwLDIuNCwwLjEsMy4zLDAuMmMxLDAuMiwyLjQsMC41LDMsMC44YzAuMSwwLDAuNCwwLjIsMC4zLDAuNWMtMC40LDEuMi0wLjcsMi0xLjEsMwoJCQkJCWMtMC4yLDAuNS0wLjUsMC4zLTAuNSwwLjNjLTEuNS0wLjUtMi45LTAuNy00LjctMC43Yy0yLjIsMC0zLjksMC43LTQuOSwyLjJjLTEuMSwxLjQtMS43LDMuMy0xLjcsNS45YzAsMi44LDAuNyw0LjgsMS45LDYuMQoJCQkJCWMxLjIsMS4zLDIuOSwxLjksNS4xLDEuOWMwLjksMCwxLjctMC4xLDIuNC0wLjJjMC43LTAuMSwxLjQtMC4zLDIuMS0wLjZDMjEwLjUsOTMuNiwyMTAuOSw5My41LDIxMS4xLDkzLjl6IE0yMzMuOCw4MC44CgkJCQkJYzEsMy40LDAuNSw2LjMsMC40LDYuNWMwLDAuNC0wLjQsMC40LTAuNCwwLjRsLTE1LjEsMGMwLjEsMi4zLDAuNiwzLjksMS44LDVjMS4xLDEuMSwyLjgsMS44LDUuMiwxLjhjMy42LDAsNS4xLTAuNyw2LjItMS4xCgkJCQkJYzAsMCwwLjQtMC4xLDAuNiwwLjNsMSwyLjhjMC4yLDAuNSwwLDAuNi0wLjEsMC43Yy0wLjksMC41LTMuMiwxLjUtNy42LDEuNWMtMi4xLDAtNC0wLjMtNS41LTAuOWMtMS41LTAuNi0yLjgtMS40LTMuOC0yLjUKCQkJCQljLTEtMS4xLTEuNy0yLjQtMi4yLTMuOGMtMC41LTEuNS0wLjctMy4xLTAuNy00LjhjMC0xLjcsMC4yLTMuMywwLjctNC44YzAuNC0xLjUsMS4xLTIuOCwyLTMuOWMwLjktMS4xLDIuMS0yLDMuNS0yLjYKCQkJCQljMS40LTAuNywzLjEtMSw1LTFjMS42LDAsMy4xLDAuMyw0LjMsMC45YzAuOSwwLjQsMS45LDEuMSwyLjksMi4yQzIzMi41LDc3LjksMjMzLjQsNzkuNCwyMzMuOCw4MC44eiBNMjE4LjgsODRoMTAuNwoJCQkJCWMtMC4xLTEuNC0wLjQtMi42LTEtMy42Yy0wLjktMS40LTIuMi0yLjItNC4yLTIuMmMtMiwwLTMuNCwwLjgtNC4zLDIuMkMyMTkuNCw4MS4zLDIxOS4xLDgyLjUsMjE4LjgsODR6IE0xMTMuMSw4MC44CgkJCQkJYzEsMy40LDAuNSw2LjMsMC41LDYuNWMwLDAuNC0wLjQsMC40LTAuNCwwLjRsLTE1LjEsMGMwLjEsMi4zLDAuNiwzLjksMS44LDVjMS4xLDEuMSwyLjgsMS44LDUuMiwxLjhjMy42LDAsNS4xLTAuNyw2LjItMS4xCgkJCQkJYzAsMCwwLjQtMC4xLDAuNiwwLjNsMSwyLjhjMC4yLDAuNSwwLDAuNi0wLjEsMC43Yy0wLjksMC41LTMuMiwxLjUtNy42LDEuNWMtMi4xLDAtNC0wLjMtNS41LTAuOWMtMS41LTAuNi0yLjgtMS40LTMuOC0yLjUKCQkJCQljLTEtMS4xLTEuNy0yLjQtMi4yLTMuOGMtMC41LTEuNS0wLjctMy4xLTAuNy00LjhjMC0xLjcsMC4yLTMuMywwLjctNC44YzAuNC0xLjUsMS4xLTIuOCwyLTMuOWMwLjktMS4xLDIuMS0yLDMuNS0yLjYKCQkJCQljMS40LTAuNywzLjEtMSw1LTFjMS42LDAsMy4xLDAuMyw0LjMsMC45YzAuOSwwLjQsMS45LDEuMSwyLjksMi4yQzExMS44LDc3LjksMTEyLjgsNzkuNCwxMTMuMSw4MC44eiBNOTguMSw4NGgxMC44CgkJCQkJYy0wLjEtMS40LTAuNC0yLjYtMS0zLjZjLTAuOS0xLjQtMi4yLTIuMi00LjItMi4yYy0yLDAtMy40LDAuOC00LjMsMi4yQzk4LjcsODEuMyw5OC40LDgyLjUsOTguMSw4NHogTTcxLjYsODMuMgoJCQkJCWMwLDAsMS4yLDAuMSwyLjUsMC4zdi0wLjZjMC0yLTAuNC0zLTEuMi0zLjZjLTAuOC0wLjYtMi4xLTEtMy43LTFjMCwwLTMuNywwLTYuNiwxLjVjLTAuMSwwLjEtMC4yLDAuMS0wLjIsMC4xCgkJCQkJcy0wLjQsMC4xLTAuNS0wLjJsLTEuMS0yLjljLTAuMi0wLjQsMC4xLTAuNiwwLjEtMC42YzEuNC0xLjEsNC42LTEuNyw0LjYtMS43YzEuMS0wLjIsMi45LTAuNCw0LTAuNGMzLDAsNS4zLDAuNyw2LjksMi4xCgkJCQkJYzEuNiwxLjQsMi40LDMuNiwyLjQsNi43bDAsMTMuOGMwLDAsMCwwLjQtMC4zLDAuNWMwLDAtMC42LDAuMi0xLjEsMC4zYy0wLjUsMC4xLTIuMywwLjUtMy44LDAuN2MtMS41LDAuMy0zLDAuNC00LjYsMC40CgkJCQkJYy0xLjUsMC0yLjgtMC4xLTQtMC40Yy0xLjItMC4zLTIuMi0wLjctMy4xLTEuM2MtMC44LTAuNi0xLjUtMS40LTItMi40Yy0wLjUtMC45LTAuNy0yLjEtMC43LTMuNGMwLTEuMywwLjMtMi41LDAuOC0zLjUKCQkJCQljMC41LTEsMS4zLTEuOCwyLjItMi41YzAuOS0wLjcsMi0xLjEsMy4xLTEuNWMxLjItMC4zLDIuNC0wLjUsMy43LTAuNUM3MC4yLDgzLjIsNzEsODMuMiw3MS42LDgzLjJ6IE02NS42LDkzLjgKCQkJCQljMCwwLDEuNCwxLjEsNC40LDAuOWMyLjItMC4xLDQuMS0wLjUsNC4xLTAuNXYtNi45YzAsMC0xLjktMC4zLTQuMS0wLjNjLTMuMSwwLTQuNCwxLjEtNC40LDEuMWMtMC45LDAuNi0xLjMsMS42LTEuMywyLjkKCQkJCQljMCwwLjgsMC4yLDEuNSwwLjUsMkM2NC45LDkzLjIsNjUsOTMuNCw2NS42LDkzLjh6IE0xOTMuMSw3NS41Yy0wLjEsMC40LTAuOSwyLjUtMS4xLDMuMmMtMC4xLDAuMy0wLjMsMC40LTAuNiwwLjQKCQkJCQljMCwwLTAuOS0wLjItMS43LTAuMmMtMC41LDAtMS4zLDAuMS0yLDAuM2MtMC43LDAuMi0xLjMsMC42LTEuOSwxLjFjLTAuNiwwLjUtMSwxLjMtMS4zLDIuMmMtMC4zLDAuOS0wLjUsMi40LTAuNSw0djExLjIKCQkJCQljMCwwLjMtMC4yLDAuNS0wLjUsMC41aC00Yy0wLjMsMC0wLjUtMC4yLTAuNS0wLjVWNzUuMmMwLTAuMywwLjItMC41LDAuNC0wLjVoMy45YzAuMywwLDAuNCwwLjIsMC40LDAuNVY3NwoJCQkJCWMwLjYtMC44LDEuNi0xLjUsMi41LTEuOWMwLjktMC40LDItMC43LDMuOS0wLjZjMSwwLjEsMi4zLDAuMywyLjUsMC40QzE5Myw3NSwxOTMuMiw3NS4xLDE5My4xLDc1LjV6IE0xNTYsNjUuMQoJCQkJCWMwLjEsMCwwLjQsMC4yLDAuMywwLjVsLTEuMiwzLjJjLTAuMSwwLjItMC4yLDAuNC0wLjcsMC4yYy0wLjEsMC0wLjMtMC4xLTAuOC0wLjJjLTAuMy0wLjEtMC44LTAuMS0xLjItMC4xCgkJCQkJYy0wLjYsMC0xLjEsMC4xLTEuNiwwLjJjLTAuNSwwLjEtMC45LDAuNC0xLjMsMC44Yy0wLjQsMC40LTAuOCwwLjktMS4xLDEuNmMtMC42LDEuNi0wLjgsMy4zLTAuOCwzLjRoNC44CgkJCQkJYzAuNCwwLDAuNSwwLjIsMC41LDAuNWwtMC42LDMuMWMtMC4xLDAuNS0wLjUsMC40LTAuNSwwLjRoLTVMMTQzLjYsOThjLTAuNCwyLTAuOCwzLjctMS4zLDUuMWMtMC41LDEuNC0xLjEsMi40LTIsMy40CgkJCQkJYy0wLjgsMC45LTEuNywxLjYtMi44LDEuOWMtMSwwLjQtMi4zLDAuNi0zLjcsMC42Yy0wLjcsMC0xLjQsMC0yLjItMC4yYy0wLjYtMC4xLTAuOS0wLjItMS40LTAuNGMtMC4yLTAuMS0wLjMtMC4zLTAuMi0wLjYKCQkJCQljMC4xLTAuMywxLTIuNywxLjEtMy4xYzAuMi0wLjQsMC41LTAuMiwwLjUtMC4yYzAuMywwLjEsMC41LDAuMiwwLjgsMC4zYzAuNCwwLjEsMC44LDAuMSwxLjIsMC4xYzAuNywwLDEuMy0wLjEsMS44LTAuMwoJCQkJCWMwLjYtMC4yLDEtMC42LDEuNC0xLjFjMC40LTAuNSwwLjctMS4yLDEuMS0yLjFjMC4zLTAuOSwwLjYtMi4yLDAuOS0zLjdsMy40LTE4LjloLTMuM2MtMC40LDAtMC41LTAuMi0wLjUtMC41bDAuNi0zLjEKCQkJCQljMC4xLTAuNSwwLjUtMC40LDAuNS0wLjRoMy40bDAuMi0xYzAuNS0zLDEuNS01LjMsMy02LjhjMS41LTEuNSwzLjctMi4zLDYuNC0yLjNjMC44LDAsMS41LDAuMSwyLjEsMC4yCgkJCQkJQzE1NSw2NC44LDE1NS41LDY0LjksMTU2LDY1LjF6IE04OC42LDk3LjZjMCwwLjMtMC4yLDAuNS0wLjQsMC41aC00Yy0wLjMsMC0wLjQtMC4yLTAuNC0wLjVWNjUuNWMwLTAuMiwwLjItMC41LDAuNC0wLjVoNAoJCQkJCWMwLjMsMCwwLjQsMC4yLDAuNCwwLjVWOTcuNnoiLz4KCQkJPC9nPgoJCTwvZz4KCTwvZz4KPC9nPgo8L3N2Zz4K diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index 8894ec2fab..c7d16166ab 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -1050,7 +1050,7 @@ export class AuthInfo extends AsyncOptionalCreatable { this.throwUserGetException(response); } else { const userInfoJson = parseJsonMap(response.body) as UserInfoResult; - const url = `${baseUrl.toString()}/services/data/${apiVersion}/sobjects/User/${userInfoJson.user_id}`; + const url = `${baseUrl.toString()}services/data/${apiVersion}/sobjects/User/${userInfoJson.user_id}`; this.logger.info(`Sending request for User SObject after successful auth code exchange to URL: ${url}`); response = await new Transport().httpRequest({ url, method: 'GET', headers }); if (response.statusCode >= 400) { diff --git a/src/webOAuthServer.ts b/src/webOAuthServer.ts index 505aebd232..9fb7771476 100644 --- a/src/webOAuthServer.ts +++ b/src/webOAuthServer.ts @@ -19,6 +19,7 @@ import { AuthInfo, DEFAULT_CONNECTED_APP_INFO } from './org'; import { SfError } from './sfError'; import { Messages } from './messages'; import { SfProjectJson } from './sfProject'; +import { EventEmitter } from 'events'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/core', 'auth'); @@ -46,6 +47,7 @@ export class WebOAuthServer extends AsyncCreatable { private webServer!: WebServer; private oauth2!: OAuth2; private oauthConfig: JwtOAuth2Config; + private oauthError = new Error('Oauth Error'); public constructor(options: WebOAuthServer.Options) { super(options); @@ -94,13 +96,12 @@ export class WebOAuthServer extends AsyncCreatable { oauth2: this.oauth2, }); await authInfo.save(); - const loginUrl = this.oauth2.loginUrl.replace(/\/+$/, ''); - const oauthSuccessUrl = `${loginUrl}/services/oauth2/success`; - this.webServer.doRedirect(303, oauthSuccessUrl, response); + await this.webServer.handleSuccess(response); response.end(); resolve(authInfo); } catch (err) { - this.webServer.reportError(err as Error, response); + this.oauthError = err as Error; + await this.webServer.handleError(response); reject(err); } }) @@ -160,9 +161,12 @@ export class WebOAuthServer extends AsyncCreatable { request.query = parseQueryString(url.query as string); if (request.query.error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const err = new SfError(request.query.error_description ?? request.query.error, request.query.error); - this.webServer.reportError(err, response); - return reject(err); + this.oauthError = new SfError( + request.query.error_description ?? request.query.error, + request.query.error + ); + await this.webServer.handleError(response); + return reject(this.oauthError); } this.logger.debug(`request.query.state: ${request.query.state as string}`); try { @@ -172,6 +176,10 @@ export class WebOAuthServer extends AsyncCreatable { } catch (err) { reject(err); } + } else if (url.pathname === '/OauthSuccess') { + this.webServer.reportSuccess(response); + } else if (url.pathname === '/OauthError') { + this.webServer.reportError(this.oauthError, response); } else { this.webServer.sendError(404, 'Resource not found', response); const errName = 'invalidRequestUri'; @@ -264,6 +272,7 @@ export class WebServer extends AsyncCreatable { public host = 'localhost'; private logger!: Logger; private sockets: Socket[] = []; + private redirectStatus = new EventEmitter(); public constructor(options: WebServer.Options) { super(options); @@ -314,7 +323,7 @@ export class WebServer extends AsyncCreatable { /** * sends a response error. * - * @param statusCode he statusCode for the response. + * @param status the statusCode for the response. * @param message the message for the http body. * @param response the response to write the error to. */ @@ -327,11 +336,12 @@ export class WebServer extends AsyncCreatable { /** * sends a response redirect. * - * @param statusCode the statusCode for the response. + * @param status the statusCode for the response. * @param url the url to redirect to. * @param response the response to write the redirect to. */ public doRedirect(status: number, url: string, response: http.ServerResponse): void { + this.logger.debug(`Redirecting to ${url}`); response.setHeader('Content-Type', 'text/plain'); const body = `${status} - Redirecting to ${url}`; response.setHeader('Content-Length', Buffer.byteLength(body)); @@ -342,20 +352,59 @@ export class WebServer extends AsyncCreatable { /** * sends a response to the browser reporting an error. * - * @param error the error - * @param response the response to write the redirect to. + * @param error the oauth error + * @param response the HTTP response. */ public reportError(error: Error, response: http.ServerResponse): void { response.setHeader('Content-Type', 'text/html'); - const body = messages.getMessage('serverErrorHTMLResponse', [error.message]); - response.setHeader('Content-Length', Buffer.byteLength(body)); + const currentYear = new Date().getFullYear(); + const encodedImg = messages.getMessage('serverSfdcImage'); + const body = messages.getMessage('serverErrorHTMLResponse', [encodedImg, error.name, error.message, currentYear]); + response.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')); + response.end(body); + if (error.stack) { + this.logger.debug(error.stack); + } + this.redirectStatus.emit('complete'); + } + + /** + * sends a response to the browser reporting the success. + * + * @param response the HTTP response. + */ + public reportSuccess(response: http.ServerResponse): void { + response.setHeader('Content-Type', 'text/html'); + const currentYear = new Date().getFullYear(); + const encodedImg = messages.getMessage('serverSfdcImage'); + const body = messages.getMessage('serverSuccessHTMLResponse', [encodedImg, currentYear]); + response.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')); response.end(body); + this.redirectStatus.emit('complete'); + } + + public async handleSuccess(response: http.ServerResponse): Promise { + return this.handleRedirect(response, '/OauthSuccess'); + } + + public async handleError(response: http.ServerResponse): Promise { + return this.handleRedirect(response, '/OauthError'); } protected async init(): Promise { this.logger = await Logger.child(this.constructor.name); } + private async handleRedirect(response: http.ServerResponse, url: '/OauthSuccess' | '/OauthError'): Promise { + return new Promise((resolve) => { + this.redirectStatus.on('complete', () => { + this.logger.debug(`Redirect complete`); + resolve(); + }); + this.doRedirect(303, url, response); + }); + } + /** * Make sure we can't open a socket on the localhost/host port. It's important because we don't want to send * auth tokens to a random strange port listener. We want to make sure we can startup our server first. diff --git a/test/unit/webOauthServerTest.ts b/test/unit/webOauthServerTest.ts index 84b1f5288e..e1ba2bce80 100644 --- a/test/unit/webOauthServerTest.ts +++ b/test/unit/webOauthServerTest.ts @@ -10,7 +10,7 @@ import * as http from 'http'; import { expect } from 'chai'; import { assert } from '@salesforce/ts-types'; -import { StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon'; +import { StubbedType, spyMethod, stubInterface, stubMethod } from '@salesforce/ts-sinon'; import { Env } from '@salesforce/kit'; import { MockTestOrgData, TestContext } from '../../src/testSetup'; import { SfProjectJson } from '../../src/sfProject'; @@ -19,6 +19,8 @@ import { AuthFields, AuthInfo } from '../../src/org/authInfo'; describe('WebOauthServer', () => { const $$ = new TestContext(); + const authCode = 'abc123456'; + describe('determineOauthPort', () => { it('should return configured oauth port if it exists', async () => { $$.SANDBOX.stub(SfProjectJson.prototype, 'get').withArgs('oauthLocalPort').returns(8080); @@ -43,7 +45,6 @@ describe('WebOauthServer', () => { describe('authorizeAndSave', () => { const testData = new MockTestOrgData(); - const oauthSuccessUrl = 'https://login.salesforce.com/services/oauth2/success'; let authFields: AuthFields; let authInfoStub: StubbedType; let serverResponseStub: StubbedType; @@ -57,32 +58,124 @@ describe('WebOauthServer', () => { }); serverResponseStub = stubInterface($$.SANDBOX, {}); - stubMethod($$.SANDBOX, WebOAuthServer.prototype, 'executeOauthRequest').callsFake(async () => serverResponseStub); authStub = stubMethod($$.SANDBOX, AuthInfo, 'create').callsFake(async () => authInfoStub); - redirectStub = stubMethod($$.SANDBOX, WebServer.prototype, 'doRedirect').callsFake(async () => {}); }); it('should save new AuthInfo', async () => { + redirectStub = stubMethod($$.SANDBOX, WebServer.prototype, 'doRedirect').callsFake(async () => {}); + stubMethod($$.SANDBOX, WebOAuthServer.prototype, 'executeOauthRequest').callsFake(async () => serverResponseStub); const oauthServer = await WebOAuthServer.create({ oauthConfig: {} }); await oauthServer.start(); + // @ts-expect-error because private member + const handleSuccessStub = stubMethod($$.SANDBOX, oauthServer.webServer, 'handleSuccess').resolves(); const authInfo = await oauthServer.authorizeAndSave(); expect(authInfoStub.save.callCount).to.equal(1); expect(authInfo.getFields()).to.deep.equal(authFields); + expect(handleSuccessStub.calledOnce).to.be.true; }); - it('should redirect to oauth success url', async () => { + it('should redirect and handle /OauthSuccess on success', async () => { const oauthServer = await WebOAuthServer.create({ oauthConfig: {} }); + const validateStateStub = stubMethod($$.SANDBOX, oauthServer, 'validateState').returns(true); await oauthServer.start(); - await oauthServer.authorizeAndSave(); + + // @ts-expect-error because private member + const webServer = oauthServer.webServer; + const reportSuccessSpy = spyMethod($$.SANDBOX, webServer, 'reportSuccess'); + + const origOn = webServer.server.on; + let requestListener: http.RequestListener; + stubMethod($$.SANDBOX, webServer.server, 'on').callsFake((event, callback) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-argument + if (event !== 'request') return origOn.call(webServer.server, event, callback); + requestListener = callback; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + callback( + { + method: 'GET', + url: `http://localhost:1717/OauthRedirect?code=${authCode}&state=972475373f51`, + query: { code: authCode }, + }, + { + setHeader: () => {}, + writeHead: () => {}, + end: () => {}, + } + ); + }); + + // stub the redirect to ensure proper redirect handling and the web server is closed. + redirectStub = stubMethod($$.SANDBOX, webServer, 'doRedirect').callsFake(async (status, url, response) => { + expect(status).to.equal(303); + expect(url).to.equal('/OauthSuccess'); + expect(response).to.be.ok; + // @ts-expect-error + await requestListener( + { method: 'GET', url: `http://localhost:1717${url}` }, + { + setHeader: () => {}, + writeHead: () => {}, + end: () => {}, + } + ); + }); + + const authInfo = await oauthServer.authorizeAndSave(); + expect(authInfo.getFields()).to.deep.equal(authFields); expect(redirectStub.callCount).to.equal(1); - expect(redirectStub.args).to.deep.equal([[303, oauthSuccessUrl, serverResponseStub]]); + expect(validateStateStub.callCount).to.equal(1); + expect(reportSuccessSpy.callCount).to.equal(1); }); - it('should report error', async () => { - const reportErrorStub = stubMethod($$.SANDBOX, WebServer.prototype, 'reportError').callsFake(async () => {}); - authStub.rejects(new Error('BAD ERROR')); + it('should redirect and handle /OauthError on error', async () => { const oauthServer = await WebOAuthServer.create({ oauthConfig: {} }); + const validateStateStub = stubMethod($$.SANDBOX, oauthServer, 'validateState').returns(true); await oauthServer.start(); + + // @ts-expect-error because private member + const webServer = oauthServer.webServer; + const reportErrorSpy = spyMethod($$.SANDBOX, webServer, 'reportError'); + + const authError = new Error('BAD ERROR'); + authStub.rejects(authError); + + const origOn = webServer.server.on; + let requestListener: http.RequestListener; + stubMethod($$.SANDBOX, webServer.server, 'on').callsFake((event, callback) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-argument + if (event !== 'request') return origOn.call(webServer.server, event, callback); + requestListener = callback; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + callback( + { + method: 'GET', + url: `http://localhost:1717/OauthRedirect?code=${authCode}&state=972475373f51`, + query: { code: authCode }, + }, + { + setHeader: () => {}, + writeHead: () => {}, + end: () => {}, + } + ); + }); + + // stub the redirect to ensure proper redirect handling and the web server is closed. + redirectStub = stubMethod($$.SANDBOX, webServer, 'doRedirect').callsFake(async (status, url, response) => { + expect(status).to.equal(303); + expect(url).to.equal('/OauthError'); + expect(response).to.be.ok; + // @ts-expect-error + await requestListener( + { method: 'GET', url: `http://localhost:1717${url}` }, + { + setHeader: () => {}, + writeHead: () => {}, + end: () => {}, + } + ); + }); + try { await oauthServer.authorizeAndSave(); assert(false, 'authorizeAndSave should fail'); @@ -90,7 +183,10 @@ describe('WebOauthServer', () => { expect((e as Error).message, 'BAD ERROR'); } expect(authStub.callCount).to.equal(1); - expect(reportErrorStub.args[0][0].message).to.equal('BAD ERROR'); + expect(redirectStub.callCount).to.equal(1); + expect(validateStateStub.callCount).to.equal(1); + expect(reportErrorSpy.callCount).to.equal(1); + expect(reportErrorSpy.args[0][0]).to.equal(authError); }); }); @@ -103,9 +199,11 @@ describe('WebOauthServer', () => { const endSpy = $$.SANDBOX.spy(); const origOn = webServer.server.on; + let requestListener: http.RequestListener; stubMethod($$.SANDBOX, webServer.server, 'on').callsFake((event, callback) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-argument if (event !== 'request') return origOn.call(webServer.server, event, callback); + requestListener = callback; // eslint-disable-next-line @typescript-eslint/no-unsafe-call callback( { @@ -114,6 +212,23 @@ describe('WebOauthServer', () => { }, { setHeader: () => {}, + writeHead: () => {}, + end: endSpy, + } + ); + }); + + // stub the redirect to ensure proper redirect handling and the web server is closed. + stubMethod($$.SANDBOX, webServer, 'doRedirect').callsFake(async (status, url, response) => { + expect(status).to.equal(303); + expect(url).to.equal('/OauthError'); + expect(response).to.be.ok; + // @ts-expect-error + await requestListener( + { method: 'GET', url: `http://localhost:1717${url}` }, + { + setHeader: () => {}, + writeHead: () => {}, end: endSpy, } ); @@ -129,7 +244,6 @@ describe('WebOauthServer', () => { }); describe('parseAuthCodeFromRequest', () => { - const authCode = 'abc123456'; let serverResponseStub: StubbedType; let serverRequestStub: StubbedType; From 040cf44420194b00ce2bd2ad7de346002747d73b Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Fri, 21 Apr 2023 09:46:53 -0600 Subject: [PATCH 3/4] chore: fix test lint errors --- test/unit/webOauthServerTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/webOauthServerTest.ts b/test/unit/webOauthServerTest.ts index e1ba2bce80..cb79820f6f 100644 --- a/test/unit/webOauthServerTest.ts +++ b/test/unit/webOauthServerTest.ts @@ -109,8 +109,8 @@ describe('WebOauthServer', () => { expect(status).to.equal(303); expect(url).to.equal('/OauthSuccess'); expect(response).to.be.ok; - // @ts-expect-error await requestListener( + // @ts-expect-error { method: 'GET', url: `http://localhost:1717${url}` }, { setHeader: () => {}, @@ -165,8 +165,8 @@ describe('WebOauthServer', () => { expect(status).to.equal(303); expect(url).to.equal('/OauthError'); expect(response).to.be.ok; - // @ts-expect-error await requestListener( + // @ts-expect-error { method: 'GET', url: `http://localhost:1717${url}` }, { setHeader: () => {}, @@ -223,8 +223,8 @@ describe('WebOauthServer', () => { expect(status).to.equal(303); expect(url).to.equal('/OauthError'); expect(response).to.be.ok; - // @ts-expect-error await requestListener( + // @ts-expect-error { method: 'GET', url: `http://localhost:1717${url}` }, { setHeader: () => {}, From 7518664884fa2e54203408786928ee308f56d05e Mon Sep 17 00:00:00 2001 From: peternhale Date: Wed, 26 Apr 2023 12:09:37 -0600 Subject: [PATCH 4/4] chore: remove plugin-login from just nuts inventory --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02ffdde057..38542eba59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,6 @@ jobs: - https://github.com/salesforcecli/plugin-schema - https://github.com/salesforcecli/plugin-env - https://github.com/salesforcecli/plugin-org - - https://github.com/salesforcecli/plugin-login with: packageName: '@salesforce/core' externalProjectGitUrl: ${{ matrix.externalProjectGitUrl }}