Skip to content

Commit

Permalink
fix(oauth2): body requests in x-www-form-urlencoded (#1066)
Browse files Browse the repository at this point in the history
According RFC 6749, all oauth2 flow sends parameters in x-www-form-url-encoded format
  • Loading branch information
visyone authored and nnixaa committed Dec 29, 2018
1 parent 7b55493 commit 3ee11f2
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 78 deletions.
226 changes: 152 additions & 74 deletions src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,17 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/token'
&& req.body['grant_type'] === NbOAuth2GrantType.AUTHORIZATION_CODE
&& req.body['code'] === 'code'
&& req.body['client_id'] === 'clientId'
&& !req.body['redirect_uri'],
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/token'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.AUTHORIZATION_CODE
&& decodeURIComponent(params['code']) === 'code'
&& decodeURIComponent(params['client_id']) === 'clientId'
&& !params['redirect_uri'])
},
)
.flush(tokenSuccessResponse);
});

it('handle error redirect back', (done: DoneFn) => {
Expand Down Expand Up @@ -189,7 +194,13 @@ describe('oauth2-auth-strategy', () => {
done();
});

httpMock.expectOne('http:https://example.com/token')
httpMock.expectOne(
req => {
return (req.url === 'http:https://example.com/token'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
)
},
)
.flush(tokenErrorResponse, { status: 400, statusText: 'Bad Request' });
});

Expand All @@ -213,12 +224,17 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/token'
&& req.headers.get('Authorization') === authHeader
&& req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN
&& req.body['refresh_token'] === successToken.getRefreshToken()
&& !req.body['scope'],
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/token'
&& req.headers.get('Authorization') === authHeader
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.REFRESH_TOKEN
&& decodeURIComponent(params['refresh_token']) === successToken.getRefreshToken()
&& !params['scope'])
},
)
.flush(tokenSuccessResponse);
});

it('handle refresh token with requestBody client auth', (done: DoneFn) => {
Expand All @@ -241,13 +257,18 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/token'
&& req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN
&& req.body['refresh_token'] === successToken.getRefreshToken()
&& req.body['client_id'] === strategy.getOption('clientId')
&& req.body['client_secret'] === strategy.getOption('clientSecret')
&& !req.body['scope'],
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/token'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.REFRESH_TOKEN
&& decodeURIComponent(params['refresh_token']) === successToken.getRefreshToken()
&& decodeURIComponent(params['client_id']) === strategy.getOption('clientId')
&& decodeURIComponent(params['client_secret']) === strategy.getOption('clientSecret')
&& !params['scope'])
},
)
.flush(tokenSuccessResponse);
});

it('handle refresh token with NO client auth', (done: DoneFn) => {
Expand All @@ -266,11 +287,16 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/token'
&& req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN
&& req.body['refresh_token'] === successToken.getRefreshToken()
&& !req.body['scope'],
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/token'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.REFRESH_TOKEN
&& decodeURIComponent(params['refresh_token']) === successToken.getRefreshToken()
&& !params['scope'])
},
)
.flush(tokenSuccessResponse);
});

it('handle refresh token and inserts existing refresh_token if needed', (done: DoneFn) => {
Expand All @@ -289,11 +315,16 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/token'
&& req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN
&& req.body['refresh_token'] === successToken.getRefreshToken()
&& !req.body['scope'],
).flush(tokenWithoutRefreshTokenResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/token'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.REFRESH_TOKEN
&& decodeURIComponent(params['refresh_token']) === successToken.getRefreshToken()
&& !params['scope'])
},
)
.flush(tokenWithoutRefreshTokenResponse);
});

it('Handle refresh-token and leaves refresh_token unchanged if present', (done: DoneFn) => {
Expand All @@ -313,11 +344,16 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/token'
&& req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN
&& req.body['refresh_token'] === successToken.getRefreshToken()
&& !req.body['scope'],
).flush(refreshedTokenResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/token'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.REFRESH_TOKEN
&& decodeURIComponent(params['refresh_token']) === successToken.getRefreshToken()
&& !params['scope'])
},
)
.flush(refreshedTokenResponse);
});

it('handle error token refresh response', (done: DoneFn) => {
Expand All @@ -335,7 +371,13 @@ describe('oauth2-auth-strategy', () => {
done();
});

httpMock.expectOne('http:https://example.com/token')
httpMock.expectOne(
req => {
return (req.url === 'http:https://example.com/token'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
)
},
)
.flush(tokenErrorResponse, { status: 400, statusText: 'Bad Request' });
});
});
Expand Down Expand Up @@ -470,12 +512,17 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/custom'
&& req.body['grant_type'] === NbOAuth2GrantType.AUTHORIZATION_CODE
&& req.body['code'] === 'code'
&& req.body['client_id'] === 'clientId'
&& req.body['redirect_uri'] === 'http:https://localhost:4200/callback',
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/custom'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.AUTHORIZATION_CODE
&& decodeURIComponent(params['code']) === 'code'
&& decodeURIComponent(params['client_id']) === 'clientId'
&& decodeURIComponent(params['redirect_uri']) === 'http:https://localhost:4200/callback')
},
)
.flush(tokenSuccessResponse);
});

it('handle success redirect and sends correct token request with BASIC client Auth', (done: DoneFn) => {
Expand All @@ -500,13 +547,18 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/custom'
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/custom'
&& req.headers.get('Authorization') === authHeader
&& req.body['grant_type'] === NbOAuth2GrantType.AUTHORIZATION_CODE
&& req.body['code'] === 'code'
&& req.body['client_id'] === 'clientId'
&& req.body['redirect_uri'] === 'http:https://localhost:4200/callback',
).flush(tokenSuccessResponse);
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.AUTHORIZATION_CODE
&& decodeURIComponent(params['code']) === 'code'
&& decodeURIComponent(params['client_id']) === 'clientId'
&& decodeURIComponent(params['redirect_uri']) === 'http:https://localhost:4200/callback')
},
)
.flush(tokenSuccessResponse);
});

it('handle success redirect and sends correct token request with REQUEST_BODY client Auth', (done: DoneFn) => {
Expand All @@ -531,13 +583,18 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/custom'
&& req.body['grant_type'] === NbOAuth2GrantType.AUTHORIZATION_CODE
&& req.body['code'] === 'code'
&& req.body['client_id'] === strategy.getOption('clientId')
&& req.body['client_secret'] === strategy.getOption('clientSecret')
&& req.body['redirect_uri'] === 'http:https://localhost:4200/callback',
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/custom'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.AUTHORIZATION_CODE
&& decodeURIComponent(params['code']) === 'code'
&& decodeURIComponent(params['client_id']) === strategy.getOption('clientId')
&& decodeURIComponent(params['client_secret']) === strategy.getOption('clientSecret')
&& decodeURIComponent(params['redirect_uri']) === 'http:https://localhost:4200/callback')
},
)
.flush(tokenSuccessResponse);
});

it('handle error redirect back', (done: DoneFn) => {
Expand Down Expand Up @@ -573,11 +630,16 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/custom'
&& req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN
&& req.body['refresh_token'] === successToken.getRefreshToken()
&& req.body['scope'] === 'read',
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/custom'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.REFRESH_TOKEN
&& decodeURIComponent(params['refresh_token']) === successToken.getRefreshToken()
&& decodeURIComponent(params['scope']) === 'read')
},
)
.flush(tokenSuccessResponse);
});

it('handle refresh token with BASIC client auth', (done: DoneFn) => {
Expand All @@ -601,12 +663,17 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/custom'
&& req.headers.get('Authorization') === authHeader
&& req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN
&& req.body['refresh_token'] === successToken.getRefreshToken()
&& req.body['scope'] === 'read',
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/custom'
&& req.headers.get('Authorization') === authHeader
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.REFRESH_TOKEN
&& decodeURIComponent(params['refresh_token']) === successToken.getRefreshToken()
&& decodeURIComponent(params['scope']) === 'read')
},
)
.flush(tokenSuccessResponse);
});

it('handle refresh token with REQUEST_BODY client auth', (done: DoneFn) => {
Expand All @@ -630,13 +697,18 @@ describe('oauth2-auth-strategy', () => {
});

httpMock.expectOne(
req => req.url === 'http:https://example.com/custom'
&& req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN
&& req.body['refresh_token'] === successToken.getRefreshToken()
&& req.body['client_id'] === strategy.getOption('clientId')
&& req.body['client_secret'] === strategy.getOption('clientSecret')
&& req.body['scope'] === 'read',
).flush(tokenSuccessResponse);
req => {
const params = parseQueryParams(req.body);
return (req.url === 'http:https://example.com/custom'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
&& decodeURIComponent(params['grant_type']) === NbOAuth2GrantType.REFRESH_TOKEN
&& decodeURIComponent(params['refresh_token']) === successToken.getRefreshToken()
&& decodeURIComponent(params['client_id']) === strategy.getOption('clientId')
&& decodeURIComponent(params['client_secret']) === strategy.getOption('clientSecret')
&& decodeURIComponent(params['scope']) === 'read')
},
)
.flush(tokenSuccessResponse);
});

it('handle error token response', (done: DoneFn) => {
Expand All @@ -656,7 +728,13 @@ describe('oauth2-auth-strategy', () => {
done();
});

httpMock.expectOne('http:https://example.com/custom')
httpMock.expectOne(
req => {
return (req.url === 'http:https://example.com/custom'
&& req.headers.get('Content-Type') === 'application/x-www-form-urlencoded'
)
},
)
.flush(tokenErrorResponse, { status: 400, statusText: 'Bad Request' });
});
});
Expand Down
14 changes: 10 additions & 4 deletions src/framework/auth/strategies/oauth2/oauth2-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,10 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
const url = this.getActionEndpoint(module);
const requireValidToken = this.getOption(`${module}.requireValidToken`);

return this.http.post(url, this.buildRefreshRequestData(token), { headers: this.buildAuthHeader() })
let headers = this.buildAuthHeader() || new HttpHeaders() ;
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');

return this.http.post(url, this.buildRefreshRequestData(token), { headers: headers })
.pipe(
map((res) => {
return new NbAuthResult(
Expand Down Expand Up @@ -275,7 +278,10 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
const url = this.getActionEndpoint(module);
const requireValidToken = this.getOption(`${module}.requireValidToken`);

return this.http.post(url, this.buildCodeRequestData(code), { headers: this.buildAuthHeader() })
let headers = this.buildAuthHeader() || new HttpHeaders() ;
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');

return this.http.post(url, this.buildCodeRequestData(code), { headers: headers })
.pipe(
map((res) => {
return new NbAuthResult(
Expand All @@ -297,7 +303,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
redirect_uri: this.getOption('token.redirectUri'),
client_id: this.getOption('clientId'),
};
return this.cleanParams(this.addCredentialsToParams(params));
return this.urlEncodeParameters(this.cleanParams(this.addCredentialsToParams(params)));
}

protected buildRefreshRequestData(token: NbAuthRefreshableToken): any {
Expand All @@ -306,7 +312,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
refresh_token: token.getRefreshToken(),
scope: this.getOption('refresh.scope'),
};
return this.cleanParams(this.addCredentialsToParams(params));
return this.urlEncodeParameters(this.cleanParams(this.addCredentialsToParams(params)));
}

protected buildPasswordRequestData(username: string, password: string ): string {
Expand Down

0 comments on commit 3ee11f2

Please sign in to comment.