@idealyst/oauth-client 0.0.1 → 1.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ import type { OAuthClient, OAuthConfig, OAuthResult } from './types'
2
+ import { Linking } from 'react-native'
3
+
4
+ export class NativeOAuthClient implements OAuthClient {
5
+ private config: OAuthConfig
6
+
7
+ constructor(config: OAuthConfig) {
8
+ this.config = config
9
+ }
10
+
11
+ async authorize(): Promise<OAuthResult> {
12
+ const state = this.generateState()
13
+ const oauthUrl = this.buildOAuthUrl(state)
14
+
15
+ // Open OAuth URL in system browser
16
+ await Linking.openURL(oauthUrl)
17
+
18
+ // Wait for deep link callback
19
+ const callbackData = await this.waitForDeepLinkCallback()
20
+
21
+ if (callbackData.error) {
22
+ throw new Error(`OAuth error: ${callbackData.error}`)
23
+ }
24
+
25
+ if (callbackData.code) {
26
+ return {
27
+ code: callbackData.code,
28
+ state: callbackData.state
29
+ }
30
+ }
31
+
32
+ throw new Error('No authorization code received')
33
+ }
34
+
35
+ private async waitForDeepLinkCallback(): Promise<{ code?: string; error?: string; state?: string }> {
36
+ return new Promise((resolve, reject) => {
37
+ let subscription: any
38
+
39
+ const handleUrl = (event: { url: string }) => {
40
+ const callbackData = this.parseDeepLink(event.url)
41
+ if (callbackData) {
42
+ cleanup()
43
+ resolve(callbackData)
44
+ }
45
+ }
46
+
47
+ const cleanup = () => {
48
+ if (subscription?.remove) {
49
+ subscription.remove()
50
+ }
51
+ }
52
+
53
+ // Check for initial URL (if app was opened from deep link)
54
+ Linking.getInitialURL().then((url: string | null) => {
55
+ if (url) {
56
+ const callbackData = this.parseDeepLink(url)
57
+ if (callbackData) {
58
+ cleanup()
59
+ resolve(callbackData)
60
+ return
61
+ }
62
+ }
63
+ })
64
+
65
+ // Listen for subsequent deep links
66
+ subscription = Linking.addEventListener('url', handleUrl)
67
+
68
+ // Timeout after 5 minutes
69
+ setTimeout(() => {
70
+ cleanup()
71
+ reject(new Error('OAuth timeout - user did not complete authorization'))
72
+ }, 5 * 60 * 1000)
73
+ })
74
+ }
75
+
76
+ private parseDeepLink(url: string): { code?: string; error?: string; state?: string } | null {
77
+ try {
78
+ const parsedUrl = new URL(url)
79
+
80
+ // Check if this is our OAuth callback
81
+ const expectedScheme = new URL(this.config.redirectUrl).protocol.slice(0, -1)
82
+ if (parsedUrl.protocol.slice(0, -1) !== expectedScheme) {
83
+ return null
84
+ }
85
+
86
+ // Extract OAuth parameters
87
+ const code = parsedUrl.searchParams.get('code')
88
+ const error = parsedUrl.searchParams.get('error')
89
+ const state = parsedUrl.searchParams.get('state')
90
+
91
+ if (!code && !error) {
92
+ return null
93
+ }
94
+
95
+ return {
96
+ code: code || undefined,
97
+ error: error || undefined,
98
+ state: state || undefined
99
+ }
100
+ } catch (error) {
101
+ return null
102
+ }
103
+ }
104
+
105
+ private buildOAuthUrl(state: string): string {
106
+ const url = new URL(this.config.oauthUrl)
107
+
108
+ url.searchParams.set('redirect_uri', this.config.redirectUrl)
109
+ url.searchParams.set('state', state)
110
+
111
+ // Add additional parameters
112
+ if (this.config.additionalParameters) {
113
+ Object.entries(this.config.additionalParameters).forEach(([key, value]) => {
114
+ url.searchParams.set(key, value)
115
+ })
116
+ }
117
+
118
+ return url.toString()
119
+ }
120
+
121
+ private generateState(): string {
122
+ // Generate random state for CSRF protection
123
+ let result = ''
124
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
125
+ for (let i = 0; i < 32; i++) {
126
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
127
+ }
128
+ return result
129
+ }
130
+ }
@@ -0,0 +1,83 @@
1
+ import type { OAuthClient, OAuthConfig, OAuthResult } from './types'
2
+
3
+ export class WebOAuthClient implements OAuthClient {
4
+ private config: OAuthConfig
5
+
6
+ constructor(config: OAuthConfig) {
7
+ this.config = config
8
+ }
9
+
10
+ async authorize(): Promise<OAuthResult> {
11
+ const state = this.generateState()
12
+
13
+ // Check if we're already in a callback
14
+ const callbackData = this.checkForCallback()
15
+
16
+ if (callbackData) {
17
+ const { code, error, returnedState } = callbackData
18
+
19
+ if (error) {
20
+ throw new Error(`OAuth error: ${error}`)
21
+ }
22
+
23
+ if (code) {
24
+ // Clean up URL
25
+ window.history.replaceState({}, document.title, window.location.pathname)
26
+
27
+ return {
28
+ code,
29
+ state: returnedState || undefined
30
+ }
31
+ }
32
+ }
33
+
34
+ // Build OAuth URL and redirect
35
+ const oauthUrl = this.buildOAuthUrl(state)
36
+ window.location.href = oauthUrl
37
+
38
+ // This won't be reached due to redirect
39
+ throw new Error('Authorization flow initiated')
40
+ }
41
+
42
+ private checkForCallback(): { code?: string; error?: string; returnedState?: string } | null {
43
+ const urlParams = new URLSearchParams(window.location.search)
44
+ const code = urlParams.get('code')
45
+ const error = urlParams.get('error')
46
+ const state = urlParams.get('state')
47
+
48
+ if (code || error) {
49
+ return {
50
+ code: code || undefined,
51
+ error: error || undefined,
52
+ returnedState: state || undefined,
53
+ }
54
+ }
55
+
56
+ return null
57
+ }
58
+
59
+ private buildOAuthUrl(state: string): string {
60
+ const url = new URL(this.config.oauthUrl)
61
+
62
+ url.searchParams.set('redirect_uri', this.config.redirectUrl)
63
+ url.searchParams.set('state', state)
64
+
65
+ // Add additional parameters
66
+ if (this.config.additionalParameters) {
67
+ Object.entries(this.config.additionalParameters).forEach(([key, value]) => {
68
+ url.searchParams.set(key, value)
69
+ })
70
+ }
71
+
72
+ return url.toString()
73
+ }
74
+
75
+ private generateState(): string {
76
+ const array = new Uint8Array(16)
77
+ crypto.getRandomValues(array)
78
+ return btoa(String.fromCharCode.apply(null, Array.from(array)))
79
+ .replace(/\+/g, '-')
80
+ .replace(/\//g, '_')
81
+ .replace(/=/g, '')
82
+ }
83
+ }
package/src/types.ts CHANGED
@@ -1,47 +1,19 @@
1
1
  export interface OAuthConfig {
2
- clientId: string
3
- redirectUrl: string
4
- additionalParameters?: Record<string, string>
5
- customHeaders?: Record<string, string>
6
- scopes?: string[]
2
+ // OAuth endpoint URL (e.g., "https://api.yourapp.com/auth/google")
3
+ oauthUrl: string
7
4
 
8
- // Provider endpoints
9
- issuer: string
10
- authorizationEndpoint?: string
11
- tokenEndpoint?: string
12
- revocationEndpoint?: string
13
- endSessionEndpoint?: string
5
+ // Redirect URL for the client app (e.g., "com.yourapp://oauth/callback")
6
+ redirectUrl: string
14
7
 
15
- // Mobile-specific
16
- androidPackageName?: string
17
- iosUrlScheme?: string
18
- }
19
-
20
- export interface OAuthTokens {
21
- accessToken: string
22
- refreshToken?: string
23
- idToken?: string
24
- tokenType?: string
25
- expiresAt?: Date
26
- scopes?: string[]
8
+ // Optional additional parameters to send to OAuth endpoint
9
+ additionalParameters?: Record<string, string>
27
10
  }
28
11
 
29
12
  export interface OAuthResult {
30
- tokens: OAuthTokens
31
- user?: any
13
+ code: string
14
+ state?: string
32
15
  }
33
16
 
34
17
  export interface OAuthClient {
35
18
  authorize(): Promise<OAuthResult>
36
- refresh(refreshToken: string): Promise<OAuthResult>
37
- revoke(token: string): Promise<void>
38
- logout(): Promise<void>
39
- getStoredTokens(): Promise<OAuthTokens | null>
40
- clearStoredTokens(): Promise<void>
41
- }
42
-
43
- export interface StorageAdapter {
44
- getItem(key: string): Promise<string | null>
45
- setItem(key: string, value: string): Promise<void>
46
- removeItem(key: string): Promise<void>
47
19
  }
@@ -1,108 +0,0 @@
1
- import { createOAuthClient, providers } from '../index'
2
-
3
- // Example: Google OAuth - works on both web and mobile
4
- export async function setupGoogleOAuth() {
5
- const client = createOAuthClient({
6
- ...providers.google,
7
- clientId: 'your-google-client-id',
8
- redirectUrl: 'com.yourapp://oauth/callback', // Mobile
9
- // redirectUrl: 'https://yourapp.com/auth/callback', // Web
10
- scopes: ['openid', 'profile', 'email'],
11
- })
12
-
13
- try {
14
- // Authorize user - same API on both platforms!
15
- const result = await client.authorize()
16
- console.log('Access token:', result.tokens.accessToken)
17
- console.log('User info:', result.user)
18
-
19
- // Token management works the same on both platforms
20
- const storedTokens = await client.getStoredTokens()
21
- if (storedTokens) {
22
- console.log('Stored tokens:', storedTokens)
23
-
24
- // Check if token needs refresh
25
- if (storedTokens.expiresAt && storedTokens.expiresAt < new Date()) {
26
- if (storedTokens.refreshToken) {
27
- const refreshed = await client.refresh(storedTokens.refreshToken)
28
- console.log('Refreshed tokens:', refreshed.tokens)
29
- }
30
- }
31
- }
32
-
33
- // Logout when done
34
- await client.logout()
35
- console.log('Logged out successfully')
36
-
37
- } catch (error) {
38
- console.error('OAuth error:', error)
39
- }
40
- }
41
-
42
- // Example: Multiple providers
43
- export async function setupMultipleProviders() {
44
- const providers = [
45
- { name: 'Google', config: { ...providers.google, clientId: 'google-client-id' } },
46
- { name: 'GitHub', config: { ...providers.github, clientId: 'github-client-id' } },
47
- { name: 'Microsoft', config: { ...providers.microsoft, clientId: 'ms-client-id' } },
48
- ]
49
-
50
- for (const provider of providers) {
51
- try {
52
- const client = createOAuthClient({
53
- ...provider.config,
54
- redirectUrl: 'com.yourapp://oauth/callback',
55
- })
56
-
57
- const result = await client.authorize()
58
- console.log(`${provider.name} login successful:`, result.tokens.accessToken)
59
- return { provider: provider.name, tokens: result.tokens }
60
- } catch (error) {
61
- console.warn(`${provider.name} login failed:`, error)
62
- }
63
- }
64
-
65
- throw new Error('All OAuth providers failed')
66
- }
67
-
68
- // Example: Custom provider configuration
69
- export async function setupCustomProvider() {
70
- const client = createOAuthClient({
71
- issuer: 'https://your-oauth-server.com',
72
- clientId: 'your-client-id',
73
- redirectUrl: 'com.yourapp://oauth/callback',
74
- scopes: ['read', 'write'],
75
- additionalParameters: {
76
- prompt: 'consent',
77
- },
78
- customHeaders: {
79
- 'X-Custom-Header': 'value',
80
- },
81
- })
82
-
83
- try {
84
- const result = await client.authorize()
85
- return result.tokens
86
- } catch (error) {
87
- console.error('Custom OAuth error:', error)
88
- throw error
89
- }
90
- }
91
-
92
- // Example: Platform-specific configuration
93
- export async function setupPlatformSpecificOAuth() {
94
- // Check if we're on mobile or web and configure accordingly
95
- const isMobile = typeof navigator !== 'undefined' && navigator.product === 'ReactNative'
96
-
97
- const client = createOAuthClient({
98
- ...providers.google,
99
- clientId: 'your-google-client-id',
100
- redirectUrl: isMobile
101
- ? 'com.yourapp://oauth/callback' // Mobile deep link
102
- : 'https://yourapp.com/auth/callback', // Web callback
103
- scopes: ['openid', 'profile', 'email'],
104
- // Note: No client secret needed - PKCE provides security for public clients
105
- })
106
-
107
- return await client.authorize()
108
- }
@@ -1,159 +0,0 @@
1
- import { authorize, refresh, revoke, type AuthConfiguration, type AuthorizeResult } from 'react-native-app-auth'
2
- import type { OAuthClient, OAuthConfig, OAuthResult, OAuthTokens, StorageAdapter } from './types'
3
-
4
- export class NativeOAuthClient implements OAuthClient {
5
- private config: OAuthConfig
6
- private storage: StorageAdapter
7
- private storageKey: string
8
- private authConfig: AuthConfiguration
9
-
10
- constructor(config: OAuthConfig, storage: StorageAdapter) {
11
- this.config = config
12
- this.storage = storage
13
- this.storageKey = `oauth_tokens_${config.clientId}`
14
-
15
- this.authConfig = {
16
- issuer: config.issuer,
17
- clientId: config.clientId,
18
- redirectUrl: config.redirectUrl,
19
- scopes: config.scopes || [],
20
- additionalParameters: config.additionalParameters || {},
21
- customHeaders: config.customHeaders || {},
22
- usesPkce: true,
23
- usesStateParam: true,
24
-
25
- // Optional endpoint overrides
26
- ...(config.authorizationEndpoint && {
27
- authorizationEndpoint: config.authorizationEndpoint
28
- }),
29
- ...(config.tokenEndpoint && {
30
- tokenEndpoint: config.tokenEndpoint
31
- }),
32
- ...(config.revocationEndpoint && {
33
- revocationEndpoint: config.revocationEndpoint
34
- }),
35
- ...(config.endSessionEndpoint && {
36
- endSessionEndpoint: config.endSessionEndpoint
37
- }),
38
- }
39
- }
40
-
41
- async authorize(): Promise<OAuthResult> {
42
- try {
43
- const result: AuthorizeResult = await authorize(this.authConfig)
44
-
45
- const tokens: OAuthTokens = {
46
- accessToken: result.accessToken,
47
- refreshToken: result.refreshToken,
48
- idToken: result.idToken,
49
- tokenType: result.tokenType || 'Bearer',
50
- expiresAt: result.accessTokenExpirationDate
51
- ? new Date(result.accessTokenExpirationDate)
52
- : undefined,
53
- scopes: result.scopes,
54
- }
55
-
56
- await this.storeTokens(tokens)
57
-
58
- return {
59
- tokens,
60
- user: result.additionalParameters
61
- }
62
- } catch (error: any) {
63
- throw new Error(`Authorization failed: ${error.message || error}`)
64
- }
65
- }
66
-
67
- async refresh(refreshToken: string): Promise<OAuthResult> {
68
- try {
69
- const result = await refresh(this.authConfig, {
70
- refreshToken,
71
- })
72
-
73
- const tokens: OAuthTokens = {
74
- accessToken: result.accessToken,
75
- refreshToken: result.refreshToken || refreshToken, // Keep original if not returned
76
- idToken: result.idToken,
77
- tokenType: result.tokenType || 'Bearer',
78
- expiresAt: result.accessTokenExpirationDate
79
- ? new Date(result.accessTokenExpirationDate)
80
- : undefined,
81
- scopes: result.scopes,
82
- }
83
-
84
- await this.storeTokens(tokens)
85
-
86
- return {
87
- tokens,
88
- user: result.additionalParameters
89
- }
90
- } catch (error: any) {
91
- throw new Error(`Token refresh failed: ${error.message || error}`)
92
- }
93
- }
94
-
95
- async revoke(token: string): Promise<void> {
96
- try {
97
- await revoke(this.authConfig, {
98
- tokenToRevoke: token,
99
- sendClientId: true,
100
- })
101
- } catch (error: any) {
102
- // Some providers return errors for already revoked tokens
103
- // Don't throw unless it's a network error
104
- if (error.message?.includes('network') || error.message?.includes('connection')) {
105
- throw new Error(`Token revocation failed: ${error.message || error}`)
106
- }
107
- }
108
- }
109
-
110
- async logout(): Promise<void> {
111
- const tokens = await this.getStoredTokens()
112
-
113
- // Revoke tokens if available
114
- if (tokens?.accessToken) {
115
- try {
116
- await this.revoke(tokens.accessToken)
117
- } catch (error) {
118
- console.warn('Failed to revoke access token:', error)
119
- }
120
- }
121
-
122
- if (tokens?.refreshToken) {
123
- try {
124
- await this.revoke(tokens.refreshToken)
125
- } catch (error) {
126
- console.warn('Failed to revoke refresh token:', error)
127
- }
128
- }
129
-
130
- await this.clearStoredTokens()
131
- }
132
-
133
- async getStoredTokens(): Promise<OAuthTokens | null> {
134
- const stored = await this.storage.getItem(this.storageKey)
135
- if (!stored) return null
136
-
137
- try {
138
- const tokens = JSON.parse(stored)
139
- return {
140
- ...tokens,
141
- expiresAt: tokens.expiresAt ? new Date(tokens.expiresAt) : undefined,
142
- }
143
- } catch {
144
- return null
145
- }
146
- }
147
-
148
- async clearStoredTokens(): Promise<void> {
149
- await this.storage.removeItem(this.storageKey)
150
- }
151
-
152
- private async storeTokens(tokens: OAuthTokens): Promise<void> {
153
- const serializable = {
154
- ...tokens,
155
- expiresAt: tokens.expiresAt?.toISOString(),
156
- }
157
- await this.storage.setItem(this.storageKey, JSON.stringify(serializable))
158
- }
159
- }
package/src/storage.ts DELETED
@@ -1,60 +0,0 @@
1
- import type { StorageAdapter } from './types'
2
-
3
- export class WebStorage implements StorageAdapter {
4
- async getItem(key: string): Promise<string | null> {
5
- if (typeof localStorage === 'undefined') {
6
- return null
7
- }
8
- return localStorage.getItem(key)
9
- }
10
-
11
- async setItem(key: string, value: string): Promise<void> {
12
- if (typeof localStorage === 'undefined') {
13
- return
14
- }
15
- localStorage.setItem(key, value)
16
- }
17
-
18
- async removeItem(key: string): Promise<void> {
19
- if (typeof localStorage === 'undefined') {
20
- return
21
- }
22
- localStorage.removeItem(key)
23
- }
24
- }
25
-
26
- export class ReactNativeStorage implements StorageAdapter {
27
- private AsyncStorage: any
28
-
29
- constructor() {
30
- try {
31
- this.AsyncStorage = require('@react-native-async-storage/async-storage').default
32
- } catch {
33
- throw new Error(
34
- 'AsyncStorage is required for React Native. Please install @react-native-async-storage/async-storage'
35
- )
36
- }
37
- }
38
-
39
- async getItem(key: string): Promise<string | null> {
40
- return await this.AsyncStorage.getItem(key)
41
- }
42
-
43
- async setItem(key: string, value: string): Promise<void> {
44
- await this.AsyncStorage.setItem(key, value)
45
- }
46
-
47
- async removeItem(key: string): Promise<void> {
48
- await this.AsyncStorage.removeItem(key)
49
- }
50
- }
51
-
52
- export function createDefaultStorage(): StorageAdapter {
53
- // Check if we're in React Native environment
54
- if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
55
- return new ReactNativeStorage()
56
- }
57
-
58
- // Default to web storage
59
- return new WebStorage()
60
- }