Skip to content

Commit 0cd48c7

Browse files
authored
Merge pull request #2 from constantine2nd/main
Google OIDC login support
2 parents dbfa19a + 2f7aaaf commit 0cd48c7

13 files changed

Lines changed: 152 additions & 9 deletions

File tree

apps/api-manager/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,8 @@ OBP_GRPC_AUTH_METADATA_VALUE_TEMPLATE=Bearer {token}
6666
# Shared-secret that lets automated tests bypass rate limiting by sending a
6767
# `Rate-Limit-Bypass-Token: <token>` header. Leave unset in production.
6868
# RATE_LIMIT_BYPASS_TOKEN=
69+
70+
# Google OIDC Configuration (used when the OBP API advertises a "google" provider in /well-known)
71+
# The Google OAuth client must list APP_CALLBACK_URL as an authorized redirect URI.
72+
# GOOGLE_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
73+
# GOOGLE_OAUTH_CLIENT_SECRET=your-client-secret

apps/api-manager/src/lib/oauth/client.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ export class OAuth2ClientWithConfig extends OAuth2Client {
135135
return {
136136
accessToken: () => tokens.access_token,
137137
refreshToken: () => tokens.refresh_token,
138+
idToken: () => {
139+
if ("id_token" in tokens && typeof tokens.id_token === "string") {
140+
return tokens.id_token;
141+
}
142+
throw new Error("Missing or invalid field 'id_token'");
143+
},
138144
accessTokenExpiresAt: () =>
139145
tokens.expires_in
140146
? new Date(Date.now() + tokens.expires_in * 1000)

apps/api-manager/src/lib/oauth/providerFactory.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,36 @@ class OBPOIDCStrategy implements OAuth2ProviderStrategy {
7272
}
7373
}
7474

75+
class GoogleStrategy implements OAuth2ProviderStrategy {
76+
providerName = "google";
77+
78+
supports(provider: string): boolean {
79+
return provider === this.providerName;
80+
}
81+
82+
getProviderName(): string {
83+
return this.providerName;
84+
}
85+
86+
async initialize(config: WellKnownUri): Promise<OAuth2ClientWithConfig> {
87+
if (!env.GOOGLE_OAUTH_CLIENT_ID || !env.GOOGLE_OAUTH_CLIENT_SECRET) {
88+
throw new Error(
89+
"GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET must be set",
90+
);
91+
}
92+
93+
const client = new OAuth2ClientWithConfig(
94+
env.GOOGLE_OAUTH_CLIENT_ID,
95+
env.GOOGLE_OAUTH_CLIENT_SECRET,
96+
env.APP_CALLBACK_URL,
97+
);
98+
99+
await client.initOIDCConfig(config.url);
100+
101+
return client;
102+
}
103+
}
104+
75105
export class OAuth2ProviderFactory {
76106
private strategies: OAuth2ProviderStrategy[] = [];
77107
private initializedClients = new Map<string, OAuth2ClientWithConfig>();
@@ -80,7 +110,7 @@ export class OAuth2ProviderFactory {
80110
// Register any available strategies
81111
this.registerStrategy(new KeyCloakStrategy());
82112
this.registerStrategy(new OBPOIDCStrategy());
83-
// Add more as needed i.e. this.registerStrategy(new GoogleStrategy());
113+
this.registerStrategy(new GoogleStrategy());
84114
}
85115

86116
registerStrategy(strategy: OAuth2ProviderStrategy): void {

apps/api-manager/src/lib/oauth/sessionHelper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,11 @@ export class SessionOAuthHelper {
113113
["openid"],
114114
);
115115

116+
// Google access tokens are opaque and can't be validated by OBP;
117+
// the id_token is the JWT that OBP verifies against Google's JWKS.
116118
await this.updateTokensInSession(
117119
session,
118-
tokens.accessToken(),
120+
provider === "google" ? tokens.idToken() : tokens.accessToken(),
119121
tokens.refreshToken() || refreshToken,
120122
);
121123

apps/api-manager/src/routes/login/obp/callback/+server.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,18 @@ export async function GET(event: RequestEvent): Promise<Response> {
174174
path: "/",
175175
});
176176

177-
const obpAccessToken = tokens.accessToken();
177+
let idToken;
178+
try {
179+
idToken = tokens.idToken();
180+
} catch (error) {
181+
// ID token might not be available for all providers/flows
182+
idToken = undefined;
183+
}
184+
185+
// Google access tokens are opaque and can't be validated by OBP;
186+
// the id_token is the JWT that OBP verifies against Google's JWKS.
187+
const obpAccessToken =
188+
provider === "google" ? idToken : tokens.accessToken();
178189

179190
logger.debug(`OBP_API_URL from config: ${OBP_API_URL}`);
180191
const currentUserUrl = `${OBP_API_URL}/users/current`;

apps/portal/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,8 @@ PUBLIC_OBP_CHAT_URL="https://chat.openbankproject.com"
122122

123123
# Shared-secret that lets automated tests bypass rate limiting by sending a
124124
# `Rate-Limit-Bypass-Token: <token>` header. Leave unset in production.
125-
# RATE_LIMIT_BYPASS_TOKEN=
125+
# RATE_LIMIT_BYPASS_TOKEN=
126+
# Google OIDC Configuration (used when the OBP API advertises a "google" provider in /well-known)
127+
# The Google OAuth client must list APP_CALLBACK_URL as an authorized redirect URI.
128+
# GOOGLE_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
129+
# GOOGLE_OAUTH_CLIENT_SECRET=your-client-secret

apps/portal/src/lib/oauth/providerFactory.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,35 @@ class OBPOIDCStrategy implements OAuth2ProviderStrategy {
7474
}
7575
}
7676

77+
class GoogleStrategy implements OAuth2ProviderStrategy {
78+
providerName = 'google';
79+
80+
supports(provider: string): boolean {
81+
return provider === this.providerName;
82+
}
83+
84+
getProviderName(): string {
85+
return this.providerName;
86+
}
87+
88+
async initialize(config: WellKnownUri): Promise<OAuth2ClientWithConfig> {
89+
if (!env.GOOGLE_OAUTH_CLIENT_ID || !env.GOOGLE_OAUTH_CLIENT_SECRET) {
90+
throw new Error('GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET must be set');
91+
}
92+
93+
const client = new OAuth2ClientWithConfig(
94+
env.GOOGLE_OAUTH_CLIENT_ID,
95+
env.GOOGLE_OAUTH_CLIENT_SECRET,
96+
env.APP_CALLBACK_URL,
97+
'google'
98+
);
99+
100+
await client.initOIDCConfig(config.url);
101+
102+
return client;
103+
}
104+
}
105+
77106
export class OAuth2ProviderFactory {
78107
private strategies: OAuth2ProviderStrategy[] = [];
79108
private initializedClients = new Map<string, OAuth2ClientWithConfig>();
@@ -82,7 +111,7 @@ export class OAuth2ProviderFactory {
82111
// Register any available strategies
83112
this.registerStrategy(new KeyCloakStrategy());
84113
this.registerStrategy(new OBPOIDCStrategy());
85-
// Add more as needed i.e. this.registerStrategy(new GoogleStrategy());
114+
this.registerStrategy(new GoogleStrategy());
86115
}
87116

88117
registerStrategy(strategy: OAuth2ProviderStrategy): void {

apps/portal/src/lib/oauth/sessionHelper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ export class SessionOAuthHelper {
8686
logger.debug(`Refreshing access token for provider: ${provider}...`);
8787
const tokens = await client.refreshAccessToken(refreshEndpoint, refreshToken, ['openid']);
8888

89+
// Google access tokens are opaque and can't be validated by OBP;
90+
// the id_token is the JWT that OBP verifies against Google's JWKS.
8991
await this.updateTokensInSession(
9092
session,
91-
tokens.accessToken(),
93+
provider === 'google' ? tokens.idToken() : tokens.accessToken(),
9294
tokens.refreshToken() || refreshToken,
9395
tokens.idToken()
9496
);

apps/portal/src/routes/login/obp/callback/+server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ export async function GET(event: RequestEvent): Promise<Response> {
158158
path: '/'
159159
});
160160

161-
const obpAccessToken = tokens.accessToken();
162161
let idToken;
163162
try {
164163
idToken = tokens.idToken();
@@ -167,6 +166,10 @@ export async function GET(event: RequestEvent): Promise<Response> {
167166
idToken = undefined;
168167
}
169168

169+
// Google access tokens are opaque and can't be validated by OBP;
170+
// the id_token is the JWT that OBP verifies against Google's JWKS.
171+
const obpAccessToken = provider === 'google' ? idToken : tokens.accessToken();
172+
170173
logger.debug(`PUBLIC_OBP_BASE_URL from env: ${env.PUBLIC_OBP_BASE_URL}`);
171174
const currentUserUrl = `${env.PUBLIC_OBP_BASE_URL}/obp/v5.1.0/users/current`;
172175
logger.info('Fetching current user from OBP:', currentUserUrl);

packages/shared/src/lib/server/oauth/client.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ export class OAuth2ClientWithConfig extends OAuth2Client {
133133
return {
134134
accessToken: () => tokens.access_token,
135135
refreshToken: () => tokens.refresh_token,
136+
idToken: () => {
137+
if ('id_token' in tokens && typeof tokens.id_token === 'string') {
138+
return tokens.id_token;
139+
}
140+
throw new Error("Missing or invalid field 'id_token'");
141+
},
136142
accessTokenExpiresAt: () =>
137143
tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : null
138144
};
@@ -219,6 +225,12 @@ export class OAuth2ClientWithConfig extends OAuth2Client {
219225
return {
220226
accessToken: () => retryTokens.access_token,
221227
refreshToken: () => retryTokens.refresh_token,
228+
idToken: () => {
229+
if ('id_token' in retryTokens && typeof retryTokens.id_token === 'string') {
230+
return retryTokens.id_token;
231+
}
232+
throw new Error("Missing or invalid field 'id_token'");
233+
},
222234
accessTokenExpiresAt: () =>
223235
retryTokens.expires_in ? new Date(Date.now() + retryTokens.expires_in * 1000) : null
224236
};
@@ -233,6 +245,12 @@ export class OAuth2ClientWithConfig extends OAuth2Client {
233245
return {
234246
accessToken: () => tokens.access_token,
235247
refreshToken: () => tokens.refresh_token,
248+
idToken: () => {
249+
if ('id_token' in tokens && typeof tokens.id_token === 'string') {
250+
return tokens.id_token;
251+
}
252+
throw new Error("Missing or invalid field 'id_token'");
253+
},
236254
accessTokenExpiresAt: () =>
237255
tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : null
238256
};

0 commit comments

Comments
 (0)