mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Verify the token belongs to the proper user. (#11593)
* Verify the user signed into the correct account * Add and fix tests * Fix tests
This commit is contained in:
@@ -145,6 +145,10 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private verifyCorrectToken(responseToken: OAuthTokenResponse, account: AzureAccount): boolean {
|
||||||
|
return this.getUserKey(responseToken.tokenClaims) === account.key.accountId;
|
||||||
|
}
|
||||||
|
|
||||||
public async getAccountSecurityToken(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> {
|
public async getAccountSecurityToken(account: AzureAccount, tenantId: string, azureResource: azdata.AzureResource): Promise<Token | undefined> {
|
||||||
if (account.isStale === true) {
|
if (account.isStale === true) {
|
||||||
Logger.log('Account was stale. No tokens being fetched.');
|
Logger.log('Account was stale. No tokens being fetched.');
|
||||||
@@ -181,6 +185,11 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
|
|
||||||
if (remainingTime < maxTolerance) {
|
if (remainingTime < maxTolerance) {
|
||||||
const result = await this.refreshToken(tenant, resource, cachedTokens.refreshToken);
|
const result = await this.refreshToken(tenant, resource, cachedTokens.refreshToken);
|
||||||
|
|
||||||
|
// Verify that the user logged into this account
|
||||||
|
if (!this.verifyCorrectToken(result, account)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
accessToken = result.accessToken;
|
accessToken = result.accessToken;
|
||||||
}
|
}
|
||||||
// Let's just return here.
|
// Let's just return here.
|
||||||
@@ -203,6 +212,10 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
// Let's try to convert the access token type, worst case we'll have to prompt the user to do an interactive authentication.
|
// Let's try to convert the access token type, worst case we'll have to prompt the user to do an interactive authentication.
|
||||||
const result = await this.refreshToken(tenant, resource, baseTokens.refreshToken);
|
const result = await this.refreshToken(tenant, resource, baseTokens.refreshToken);
|
||||||
|
// Verify that the user logged into this account
|
||||||
|
if (!this.verifyCorrectToken(result, account)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (result.accessToken) {
|
if (result.accessToken) {
|
||||||
return {
|
return {
|
||||||
...result.accessToken,
|
...result.accessToken,
|
||||||
@@ -258,6 +271,26 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
return this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
|
return this.getTokenHelper(tenant, resource, accessTokenString, refreshTokenString, expiresOnString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getUserKey(tokenClaims: TokenClaims): string {
|
||||||
|
// Personal accounts don't have an oid when logging into the `common` tenant, but when logging into their home tenant they end up having an oid.
|
||||||
|
// This makes the key for the same account be different.
|
||||||
|
// We need to special case personal accounts.
|
||||||
|
|
||||||
|
let userKey: string;
|
||||||
|
if (tokenClaims.idp === 'live.com') { // Personal account
|
||||||
|
userKey = tokenClaims.unique_name ?? tokenClaims.email ?? tokenClaims.sub;
|
||||||
|
} else {
|
||||||
|
userKey = tokenClaims.home_oid ?? tokenClaims.oid ?? tokenClaims.unique_name ?? tokenClaims.email ?? tokenClaims.sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userKey) {
|
||||||
|
Logger.pii(tokenClaims);
|
||||||
|
throw new AzureAuthError(localize('azure.userKeyUndefined', "User key was undefined - could not create a userKey from the tokenClaims"), 'user key undefined', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userKey;
|
||||||
|
}
|
||||||
|
|
||||||
public async getTokenHelper(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise<OAuthTokenResponse> {
|
public async getTokenHelper(tenant: Tenant, resource: Resource, accessTokenString: string, refreshTokenString: string, expiresOnString: string): Promise<OAuthTokenResponse> {
|
||||||
if (!accessTokenString) {
|
if (!accessTokenString) {
|
||||||
const msg = localize('azure.accessTokenEmpty', 'No access token returned from Microsoft OAuth');
|
const msg = localize('azure.accessTokenEmpty', 'No access token returned from Microsoft OAuth');
|
||||||
@@ -265,16 +298,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tokenClaims: TokenClaims = this.getTokenClaims(accessTokenString);
|
const tokenClaims: TokenClaims = this.getTokenClaims(accessTokenString);
|
||||||
let userKey: string;
|
const userKey = this.getUserKey(tokenClaims);
|
||||||
|
|
||||||
// Personal accounts don't have an oid when logging into the `common` tenant, but when logging into their home tenant they end up having an oid.
|
|
||||||
// This makes the key for the same account be different.
|
|
||||||
// We need to special case personal accounts.
|
|
||||||
if (tokenClaims.idp === 'live.com') { // Personal account
|
|
||||||
userKey = tokenClaims.unique_name ?? tokenClaims.email ?? tokenClaims.sub;
|
|
||||||
} else {
|
|
||||||
userKey = tokenClaims.home_oid ?? tokenClaims.oid ?? tokenClaims.unique_name ?? tokenClaims.email ?? tokenClaims.sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userKey) {
|
if (!userKey) {
|
||||||
const msg = localize('azure.noUniqueIdentifier', "The user had no unique identifier within AAD");
|
const msg = localize('azure.noUniqueIdentifier', "The user had no unique identifier within AAD");
|
||||||
@@ -415,7 +439,6 @@ export abstract class AzureAuth implements vscode.Disposable {
|
|||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
const result = await this.login(tenant, resource);
|
const result = await this.login(tenant, resource);
|
||||||
result?.authComplete?.resolve();
|
result?.authComplete?.resolve();
|
||||||
return result?.response;
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ let mockRefreshToken: RefreshToken;
|
|||||||
const mockClaims = {
|
const mockClaims = {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
email: 'example@example.com',
|
email: 'example@example.com',
|
||||||
sub: 'someUniqueId'
|
sub: 'someUniqueId',
|
||||||
|
idp: 'idp',
|
||||||
|
oid: 'userUniqueKey'
|
||||||
} as TokenClaims;
|
} as TokenClaims;
|
||||||
|
|
||||||
const mockTenant: Tenant = {
|
const mockTenant: Tenant = {
|
||||||
@@ -55,6 +57,9 @@ describe('Azure Authentication', function () {
|
|||||||
// authDeviceCode.callBase = true;
|
// authDeviceCode.callBase = true;
|
||||||
|
|
||||||
mockAccount = {
|
mockAccount = {
|
||||||
|
key: {
|
||||||
|
accountId: mockClaims.oid
|
||||||
|
},
|
||||||
isStale: false,
|
isStale: false,
|
||||||
properties: {
|
properties: {
|
||||||
tenants: [mockTenant]
|
tenants: [mockTenant]
|
||||||
@@ -159,7 +164,8 @@ describe('Azure Authentication', function () {
|
|||||||
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken));
|
const mockToken: AccessToken = JSON.parse(JSON.stringify(mockAccessToken));
|
||||||
delete (mockToken as any).invalidData;
|
delete (mockToken as any).invalidData;
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
accessToken: mockToken
|
accessToken: mockToken,
|
||||||
|
tokenClaims: mockClaims
|
||||||
} as OAuthTokenResponse);
|
} as OAuthTokenResponse);
|
||||||
});
|
});
|
||||||
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
const securityToken = await azureAuthCodeGrant.object.getAccountSecurityToken(mockAccount, mockTenant.id, AzureResource.MicrosoftResourceManagement);
|
||||||
@@ -192,14 +198,15 @@ describe('Azure Authentication', function () {
|
|||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
accessToken: mockAccessToken,
|
accessToken: mockAccessToken,
|
||||||
refreshToken: mockRefreshToken,
|
refreshToken: mockRefreshToken,
|
||||||
expiresOn: ''
|
expiresOn: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
delete (mockAccessToken as any).tokenType;
|
delete (mockAccessToken as any).tokenType;
|
||||||
|
|
||||||
azureAuthCodeGrant.setup(x => x.refreshToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
azureAuthCodeGrant.setup(x => x.refreshToken(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
accessToken: mockAccessToken
|
accessToken: mockAccessToken,
|
||||||
|
tokenClaims: mockClaims,
|
||||||
} as OAuthTokenResponse);
|
} as OAuthTokenResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -273,4 +280,77 @@ describe('Azure Authentication', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getUserKey', function () {
|
||||||
|
let tokenClaims: TokenClaims;
|
||||||
|
beforeEach(function () {
|
||||||
|
tokenClaims = {
|
||||||
|
unique_name: 'unique_name',
|
||||||
|
email: 'email',
|
||||||
|
sub: 'sub',
|
||||||
|
oid: 'oid',
|
||||||
|
home_oid: 'home_oid'
|
||||||
|
} as TokenClaims;
|
||||||
|
});
|
||||||
|
describe('personal accounts - live.com', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
tokenClaims.idp = 'live.com';
|
||||||
|
});
|
||||||
|
it('prefer unique_name', function () {
|
||||||
|
const value = azureAuthCodeGrant.object.getUserKey(tokenClaims);
|
||||||
|
|
||||||
|
should(value).be.equal(tokenClaims.unique_name);
|
||||||
|
});
|
||||||
|
it('fallback to email', function () {
|
||||||
|
delete tokenClaims.unique_name;
|
||||||
|
const value = azureAuthCodeGrant.object.getUserKey(tokenClaims);
|
||||||
|
|
||||||
|
should(value).be.equal(tokenClaims.email);
|
||||||
|
});
|
||||||
|
it('fallback to sub', function () {
|
||||||
|
delete tokenClaims.unique_name;
|
||||||
|
delete tokenClaims.email;
|
||||||
|
const value = azureAuthCodeGrant.object.getUserKey(tokenClaims);
|
||||||
|
|
||||||
|
should(value).be.equal(tokenClaims.sub);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('work accounts', function () {
|
||||||
|
it('prefer home_oid', function () {
|
||||||
|
const value = azureAuthCodeGrant.object.getUserKey(tokenClaims);
|
||||||
|
|
||||||
|
should(value).be.equal(tokenClaims.home_oid);
|
||||||
|
});
|
||||||
|
it('fallback to oid', function () {
|
||||||
|
delete tokenClaims.home_oid;
|
||||||
|
const value = azureAuthCodeGrant.object.getUserKey(tokenClaims);
|
||||||
|
|
||||||
|
should(value).be.equal(tokenClaims.oid);
|
||||||
|
});
|
||||||
|
it('fallback to unique_name', function () {
|
||||||
|
delete tokenClaims.home_oid;
|
||||||
|
delete tokenClaims.oid;
|
||||||
|
const value = azureAuthCodeGrant.object.getUserKey(tokenClaims);
|
||||||
|
|
||||||
|
should(value).be.equal(tokenClaims.unique_name);
|
||||||
|
});
|
||||||
|
it('fallback to email', function () {
|
||||||
|
delete tokenClaims.home_oid;
|
||||||
|
delete tokenClaims.oid;
|
||||||
|
delete tokenClaims.unique_name;
|
||||||
|
const value = azureAuthCodeGrant.object.getUserKey(tokenClaims);
|
||||||
|
|
||||||
|
should(value).be.equal(tokenClaims.email);
|
||||||
|
});
|
||||||
|
it('fallback to sub', function () {
|
||||||
|
delete tokenClaims.home_oid;
|
||||||
|
delete tokenClaims.oid;
|
||||||
|
delete tokenClaims.unique_name;
|
||||||
|
delete tokenClaims.email;
|
||||||
|
const value = azureAuthCodeGrant.object.getUserKey(tokenClaims);
|
||||||
|
|
||||||
|
should(value).be.equal(tokenClaims.sub);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user