Skip to content

Commit

Permalink
Merge pull request #3329 from activepieces/feat/granttype
Browse files Browse the repository at this point in the history
feat: add client credentials oauth2
  • Loading branch information
abuaboud committed Dec 5, 2023
2 parents 7110d7e + a1ba2a2 commit 9beaf6e
Show file tree
Hide file tree
Showing 15 changed files with 92 additions and 28 deletions.
5 changes: 5 additions & 0 deletions docs/developers/piece-reference/authentication.mdx
Expand Up @@ -119,9 +119,14 @@ This authentication collects OAuth2 authentication details, including the authen
```typescript
PieceAuth.OAuth2({
displayName: 'OAuth2 Authentication',
grantType: OAuth2GrantType.AUTHORIZATION_CODE,
required: true,
authUrl: 'https://example.com/auth',
tokenUrl: 'https://example.com/token',
scope: ['read', 'write']
})
```

<Tip>
Please note `OAuth2GrantType.CLIENT_CREDENTIALS` is also supported for service-based authentication.
</Tip>
Expand Up @@ -10,6 +10,7 @@ import {
EngineResponseStatus,
ErrorCode,
ExecuteValidateAuthOperation,
OAuth2GrantType,
ProjectId,
SeekPage,
UpsertAppConnectionRequestBody,
Expand Down Expand Up @@ -181,6 +182,7 @@ const validateConnectionValue = async (
projectId,
pieceName: connection.appName,
request: {
grantType: OAuth2GrantType.AUTHORIZATION_CODE,
code: connection.value.code,
clientId: connection.value.client_id,
tokenUrl: connection.value.token_url!,
Expand All @@ -194,6 +196,7 @@ const validateConnectionValue = async (
projectId,
pieceName: connection.appName,
request: {
grantType: OAuth2GrantType.AUTHORIZATION_CODE,
code: connection.value.code,
clientId: connection.value.client_id,
tokenUrl: connection.value.token_url!,
Expand All @@ -209,6 +212,7 @@ const validateConnectionValue = async (
code: connection.value.code,
clientId: connection.value.client_id,
tokenUrl: connection.value.token_url!,
grantType: connection.value.grant_type!,
redirectUrl: connection.value.redirect_url,
clientSecret: connection.value.client_secret,
authorizationMethod: connection.value.authorization_method,
Expand Down
@@ -1,5 +1,5 @@
import { OAuth2AuthorizationMethod } from '@activepieces/pieces-framework'
import { BaseOAuth2ConnectionValue } from '@activepieces/shared'
import { BaseOAuth2ConnectionValue, OAuth2GrantType } from '@activepieces/shared'

export type OAuth2Service<CONNECTION_VALUE extends BaseOAuth2ConnectionValue> = {
claim(request: ClaimOAuth2Request): Promise<CONNECTION_VALUE>
Expand All @@ -12,17 +12,20 @@ export type RefreshOAuth2Request<T extends BaseOAuth2ConnectionValue> = {
connectionValue: T
}

export type OAuth2RequestBody = {
code: string
clientId: string
tokenUrl: string
clientSecret?: string
redirectUrl?: string
grantType?: OAuth2GrantType
authorizationMethod?: OAuth2AuthorizationMethod
codeVerifier?: string
}

export type ClaimOAuth2Request = {
projectId: string
pieceName: string
request: {
code: string
clientId: string
tokenUrl: string
clientSecret?: string
redirectUrl?: string
authorizationMethod?: OAuth2AuthorizationMethod
codeVerifier?: string
}
request: OAuth2RequestBody
}

@@ -1,4 +1,4 @@
import { BaseOAuth2ConnectionValue, deleteProps } from '@activepieces/shared'
import { BaseOAuth2ConnectionValue, OAuth2GrantType, deleteProps } from '@activepieces/shared'

export const oauth2Util = {
formatOAuth2Response,
Expand All @@ -7,7 +7,8 @@ export const oauth2Util = {

function isExpired(connection: BaseOAuth2ConnectionValue): boolean {
const secondsSinceEpoch = Math.round(Date.now() / 1000)
if (!connection.refresh_token) {
const grantType = connection.grant_type ?? OAuth2GrantType.AUTHORIZATION_CODE
if (grantType === OAuth2GrantType.AUTHORIZATION_CODE && !connection.refresh_token) {
return false
}
// Salesforce doesn't provide an 'expires_in' field, as it is dynamic per organization; therefore, it's necessary for us to establish a low threshold and consistently refresh it.
Expand All @@ -20,7 +21,7 @@ function isExpired(connection: BaseOAuth2ConnectionValue): boolean {



function formatOAuth2Response(response: Omit<BaseOAuth2ConnectionValue, 'claimed_at'> ): BaseOAuth2ConnectionValue {
function formatOAuth2Response(response: Omit<BaseOAuth2ConnectionValue, 'claimed_at'>): BaseOAuth2ConnectionValue {
const secondsSinceEpoch = Math.round(Date.now() / 1000)
const formattedResponse: BaseOAuth2ConnectionValue = {
...response,
Expand Down
@@ -1,5 +1,5 @@
import { OAuth2AuthorizationMethod } from '@activepieces/pieces-framework'
import { ActivepiecesError, AppConnectionType, BaseOAuth2ConnectionValue, ErrorCode, OAuth2ConnectionValueWithApp, isNil } from '@activepieces/shared'
import { ActivepiecesError, AppConnectionType, BaseOAuth2ConnectionValue, ErrorCode, OAuth2ConnectionValueWithApp, OAuth2GrantType, isNil } from '@activepieces/shared'
import axios from 'axios'
import { oauth2Util } from '../oauth2-util'
import { logger } from '../../../../helper/logger'
Expand All @@ -14,10 +14,18 @@ export const credentialsOauth2Service: OAuth2Service<OAuth2ConnectionValueWithAp

async function claim({ request }: ClaimOAuth2Request): Promise<OAuth2ConnectionValueWithApp> {
try {
const grantType = request.grantType ?? OAuth2GrantType.AUTHORIZATION_CODE
const body: Record<string, string> = {
redirect_uri: request.redirectUrl!,
grant_type: 'authorization_code',
code: request.code,
grant_type: grantType,
}
switch (grantType) {
case OAuth2GrantType.AUTHORIZATION_CODE: {
body.redirect_uri = request.redirectUrl!
body.code = request.code
break
}
case OAuth2GrantType.CLIENT_CREDENTIALS:
break
}
if (request.codeVerifier) {
body.code_verifier = request.codeVerifier
Expand Down Expand Up @@ -52,6 +60,7 @@ async function claim({ request }: ClaimOAuth2Request): Promise<OAuth2ConnectionV
client_id: request.clientId,
client_secret: request.clientSecret!,
redirect_url: request.redirectUrl!,
grant_type: grantType,
authorization_method: authorizationMethod,
}
}
Expand All @@ -62,7 +71,7 @@ async function claim({ request }: ClaimOAuth2Request): Promise<OAuth2ConnectionV
params: {
clientId: request.clientId,
tokenUrl: request.tokenUrl,
redirectUrl: request.redirectUrl!,
redirectUrl: request.redirectUrl ?? '',
},
})
}
Expand All @@ -77,10 +86,21 @@ async function refresh(
if (!oauth2Util.isExpired(appConnection)) {
return appConnection
}
const body: Record<string, string> = {
grant_type: 'refresh_token',
refresh_token: appConnection.refresh_token,
const body: Record<string, string> = {}
switch (connectionValue.grant_type) {
case OAuth2GrantType.AUTHORIZATION_CODE: {
body.grant_type = 'refresh_token'
body.refresh_token = appConnection.refresh_token
break
}
case OAuth2GrantType.CLIENT_CREDENTIALS: {
body.grant_type = connectionValue.grant_type
break
}
default:
throw new Error(`Unknown grant type: ${connectionValue.grant_type}`)
}

const headers: Record<string, string> = {
'content-type': 'application/x-www-form-urlencoded',
accept: 'application/json',
Expand Down
Expand Up @@ -13,6 +13,7 @@ import { ModalCommunicationService } from '../service/modal-communication.servic
import { AuthService } from '../service/auth.service';
import { FormControl } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { OAuth2GrantType, assertNotEqual } from '@activepieces/shared';

@Component({
selector: 'app-enable-integration-modal',
Expand Down Expand Up @@ -86,6 +87,7 @@ export class EnableIntegrationModalComponent {
OAuth2Settings.tokenUrl = OAuth2Settings.tokenUrl.replace('login.', 'test.');
}
}
assertNotEqual(OAuth2Settings.grantType, OAuth2GrantType.CLIENT_CREDENTIALS, 'OAuth2GrantType', 'AUTHORIZATION_CODE');
this.popUpOpen$ = this.authenticationService
.openPopUp(OAuth2Settings)
.pipe(
Expand All @@ -109,6 +111,7 @@ export class EnableIntegrationModalComponent {
throw err;
})
);

}
}

Expand Down
@@ -1,11 +1,12 @@
import { BaseModel, ProjectId } from "@activepieces/shared";
import { BaseModel, OAuth2GrantType, ProjectId } from "@activepieces/shared";

export type AppCredentialId = string;

export interface AppOAuth2Settings {
type: AppCredentialType.OAUTH2;
authUrl: string;
tokenUrl: string;
grantType: OAuth2GrantType,
clientId: string;
clientSecret?: string;
scope: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/pieces/framework/package.json
@@ -1,5 +1,5 @@
{
"name": "@activepieces/pieces-framework",
"version": "0.7.3",
"version": "0.7.4",
"type": "commonjs"
}
4 changes: 3 additions & 1 deletion packages/pieces/framework/src/lib/property/oauth2-prop.ts
Expand Up @@ -4,6 +4,7 @@ import { BasePieceAuthSchema, SecretTextProperty, ShortTextProperty, TPropertyVa
import { StaticDropdownProperty } from "./dropdown-prop";
import { StaticPropsValue } from "./property";
import { ValidationInputType } from "../validators/types";
import { OAuth2GrantType } from "@activepieces/shared";

type OAuthProp = ShortTextProperty<true> | SecretTextProperty<true> | StaticDropdownProperty<any, true>;

Expand All @@ -20,7 +21,8 @@ export type OAuth2PropertySchema = BasePieceAuthSchema<OAuth2PropertyValue> & {
scope: string[];
pkce?: boolean;
authorizationMethod?: OAuth2AuthorizationMethod,
extra?: Record<string, unknown>
grantType?: OAuth2GrantType;
extra?: Record<string, unknown>;
}

export type OAuth2PropertyValue<T extends OAuth2Props = any> = {
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/package.json
@@ -1,5 +1,5 @@
{
"name": "@activepieces/shared",
"version": "0.10.33",
"version": "0.10.34",
"type": "commonjs"
}
3 changes: 2 additions & 1 deletion packages/shared/src/lib/app-connection/app-connection.ts
Expand Up @@ -2,6 +2,7 @@ import { Static, Type } from '@sinclair/typebox'
import { BaseModel, BaseModelSchema } from '../common/base-model'
import { OAuth2AuthorizationMethod } from './oauth2-authorization-method'
import { ApId } from '../common/id-generator'
import { OAuth2GrantType } from './dto/upsert-app-connection-request'

export type AppConnectionId = string

Expand Down Expand Up @@ -41,7 +42,7 @@ export type BaseOAuth2ConnectionValue = {
authorization_method?: OAuth2AuthorizationMethod
data: Record<string, unknown>
props?: Record<string, unknown>

grant_type?: OAuth2GrantType
}

export type CustomAuthConnectionValue = {
Expand Down
Expand Up @@ -7,6 +7,11 @@ const commonAuthProps = {
appName: Type.String({}),
}

export enum OAuth2GrantType {
AUTHORIZATION_CODE = 'authorization_code',
CLIENT_CREDENTIALS = 'client_credentials',
}

export const UpsertCustomAuthRequest = Type.Object({
...commonAuthProps,
type: Type.Literal(AppConnectionType.CUSTOM_AUTH),
Expand Down Expand Up @@ -63,6 +68,7 @@ export const UpsertOAuth2Request = Type.Object({
value: Type.Object({
client_id: Type.String({}),
client_secret: Type.String({}),
grant_type: Type.Optional(Type.Enum(OAuth2GrantType)),
token_url: Type.String({}),
props: Type.Optional(Type.Record(Type.String(), Type.Any())),
scope: Type.String(),
Expand Down
12 changes: 12 additions & 0 deletions packages/shared/src/lib/common/utils/assertions.ts
Expand Up @@ -18,6 +18,18 @@ export function assertEqual<T>(
}
}

export function assertNotEqual<T>(
value1: T,
value2: T,
fieldName1: string,
fieldName2: string,
): void {
if (value1 === value2) {
throw new Error(`${fieldName1} and ${fieldName2} should not be equal`)
}
}


export const isNotUndefined = <T>(value: T | undefined): value is T => {
return value !== undefined
}
Expand Up @@ -29,7 +29,8 @@
</ng-template></mat-error>
</mat-form-field>

<mat-form-field class="ap-w-full" appearance="outline">
<mat-form-field class="ap-w-full" appearance="outline"
*ngIf="dialogData.pieceAuthProperty.grantType !== OAuth2GrantType.CLIENT_CREDENTIALS">
<mat-label>Redirect URL</mat-label>
<input placeholder="Redirect URL" [matTooltip]="redirectUrlTooltip" formControlName="redirect_url" matInput
type="text" />
Expand Down Expand Up @@ -78,7 +79,7 @@

</div>

<div>
<div *ngIf="dialogData.pieceAuthProperty.grantType !== OAuth2GrantType.CLIENT_CREDENTIALS">
<app-o-auth2-connect-control [popupParams]="getOAuth2Settings()"
[settingsValid]="authenticationSettingsControlsValid" formControlName="value"></app-o-auth2-connect-control>
<p @fadeInUp class="ap-typography-caption ap-text-danger"
Expand Down
Expand Up @@ -13,6 +13,7 @@ import { catchError, map, Observable, of, take, tap } from 'rxjs';
import {
AppConnectionType,
AppConnectionWithoutSensitiveData,
OAuth2GrantType,
UpsertOAuth2Request,
} from '@activepieces/shared';
import {
Expand Down Expand Up @@ -60,6 +61,7 @@ export interface OAuth2ConnectionDialogData {
export class OAuth2ConnectionDialogComponent implements OnInit {
PropertyType = PropertyType;
readonly FAKE_CODE = 'FAKE_CODE';
readonly OAuth2GrantType = OAuth2GrantType;
settingsForm: FormGroup<OAuth2PropertySettings>;
loading = false;
submitted = false;
Expand Down Expand Up @@ -170,6 +172,9 @@ export class OAuth2ConnectionDialogComponent implements OnInit {
code: this.settingsForm.controls.value.value.code,
code_challenge: this.settingsForm.controls.value.value.code_challenge,
type: AppConnectionType.OAUTH2,
grant_type:
this.dialogData.pieceAuthProperty.grantType ??
OAuth2GrantType.AUTHORIZATION_CODE,
authorization_method:
this.dialogData.pieceAuthProperty.authorizationMethod,
client_id: this.settingsForm.controls.client_id.value,
Expand Down

0 comments on commit 9beaf6e

Please sign in to comment.