@atproto/oauth-client-expo 0.0.1
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.
- package/CHANGELOG.md +11 -0
- package/LICENSE.txt +7 -0
- package/README.md +140 -0
- package/android/.editorconfig +2 -0
- package/android/build.gradle +47 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/atprotooauthclient/Crypto.kt +69 -0
- package/android/src/main/java/expo/modules/atprotooauthclient/ExpoAtprotoOAuthClientModule.kt +41 -0
- package/android/src/main/java/expo/modules/atprotooauthclient/Jose.kt +116 -0
- package/android/src/main/java/expo/modules/atprotooauthclient/Records.kt +61 -0
- package/dist/ExpoAtprotoOAuthClientModule.d.ts +22 -0
- package/dist/ExpoAtprotoOAuthClientModule.d.ts.map +1 -0
- package/dist/ExpoAtprotoOAuthClientModule.js +3 -0
- package/dist/ExpoAtprotoOAuthClientModule.js.map +1 -0
- package/dist/ExpoAtprotoOAuthClientModule.types.d.ts +2 -0
- package/dist/ExpoAtprotoOAuthClientModule.types.d.ts.map +1 -0
- package/dist/ExpoAtprotoOAuthClientModule.types.js +2 -0
- package/dist/ExpoAtprotoOAuthClientModule.types.js.map +1 -0
- package/dist/expo-oauth-client-interface.d.ts +6 -0
- package/dist/expo-oauth-client-interface.d.ts.map +1 -0
- package/dist/expo-oauth-client-interface.js +2 -0
- package/dist/expo-oauth-client-interface.js.map +1 -0
- package/dist/expo-oauth-client-options.d.ts +9 -0
- package/dist/expo-oauth-client-options.d.ts.map +1 -0
- package/dist/expo-oauth-client-options.js +2 -0
- package/dist/expo-oauth-client-options.js.map +1 -0
- package/dist/expo-oauth-client.native.d.ts +13 -0
- package/dist/expo-oauth-client.native.d.ts.map +1 -0
- package/dist/expo-oauth-client.native.js +130 -0
- package/dist/expo-oauth-client.native.js.map +1 -0
- package/dist/expo-oauth-client.web.d.ts +9 -0
- package/dist/expo-oauth-client.web.d.ts.map +1 -0
- package/dist/expo-oauth-client.web.js +24 -0
- package/dist/expo-oauth-client.web.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/polyfill.d.ts +5 -0
- package/dist/polyfill.d.ts.map +1 -0
- package/dist/polyfill.js +5 -0
- package/dist/polyfill.js.map +1 -0
- package/dist/utils/expo-key.d.ts +11 -0
- package/dist/utils/expo-key.d.ts.map +1 -0
- package/dist/utils/expo-key.js +29 -0
- package/dist/utils/expo-key.js.map +1 -0
- package/dist/utils/mmkv-simple-store-ttl.d.ts +24 -0
- package/dist/utils/mmkv-simple-store-ttl.d.ts.map +1 -0
- package/dist/utils/mmkv-simple-store-ttl.js +62 -0
- package/dist/utils/mmkv-simple-store-ttl.js.map +1 -0
- package/dist/utils/mmkv-simple-store.d.ts +18 -0
- package/dist/utils/mmkv-simple-store.d.ts.map +1 -0
- package/dist/utils/mmkv-simple-store.js +31 -0
- package/dist/utils/mmkv-simple-store.js.map +1 -0
- package/dist/utils/stores.d.ts +24 -0
- package/dist/utils/stores.d.ts.map +1 -0
- package/dist/utils/stores.js +99 -0
- package/dist/utils/stores.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Crypto.swift +83 -0
- package/ios/ExpoAtprotoOAuthClient.podspec +31 -0
- package/ios/ExpoAtprotoOAuthClientModule.swift +45 -0
- package/ios/Jose.swift +137 -0
- package/ios/Records.swift +58 -0
- package/package.json +52 -0
- package/src/ExpoAtprotoOAuthClientModule.ts +33 -0
- package/src/ExpoAtprotoOAuthClientModule.types.ts +2 -0
- package/src/expo-oauth-client-interface.ts +10 -0
- package/src/expo-oauth-client-options.ts +27 -0
- package/src/expo-oauth-client.d.ts +6 -0
- package/src/expo-oauth-client.native.ts +111 -0
- package/src/expo-oauth-client.web.ts +42 -0
- package/src/index.ts +4 -0
- package/src/polyfill.ts +4 -0
- package/src/utils/expo-key.ts +50 -0
- package/src/utils/mmkv-simple-store-ttl.ts +90 -0
- package/src/utils/mmkv-simple-store.ts +48 -0
- package/src/utils/stores.ts +115 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +4 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @atproto/oauth-client-expo
|
|
2
|
+
|
|
3
|
+
## 0.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#4220](https://github.com/bluesky-social/atproto/pull/4220) [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - OAuthClient SDK for Expo
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`fefe70126`](https://github.com/bluesky-social/atproto/commit/fefe70126d0ea82507ac750f669b3478290f186b), [`09439d7d6`](https://github.com/bluesky-social/atproto/commit/09439d7d688294ad1a0c78a74b901ba2f7c5f4c3)]:
|
|
10
|
+
- @atproto/oauth-client-browser@0.3.33
|
|
11
|
+
- @atproto/oauth-client@0.5.7
|
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Dual MIT/Apache-2.0 License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
|
|
4
|
+
|
|
5
|
+
Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
|
|
6
|
+
|
|
7
|
+
Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Expo Atproto OAuth
|
|
2
|
+
|
|
3
|
+
This is an Expo client library for Atproto OAuth. It implements the required
|
|
4
|
+
native crypto functions for supporting JWTs in React Native and uses the base
|
|
5
|
+
`OAuthClient` interface found in [the Atproto repository](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client).
|
|
6
|
+
|
|
7
|
+
### In bare React Native projects
|
|
8
|
+
|
|
9
|
+
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/)
|
|
10
|
+
before continuing.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Once you have satisfied the prerequisites, you can simply install the library with `npm install --save @atproto/oauth-client-expo`.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Serve your `oauth-client-metadata.json`
|
|
19
|
+
|
|
20
|
+
You will need to server an `oauth-client-metadata.json` from your application's website. An example of this metadata
|
|
21
|
+
would look like this:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
// assets/oauth-client-metadata.json
|
|
25
|
+
{
|
|
26
|
+
"client_id": "https://example.com/oauth-client-metadata.json",
|
|
27
|
+
"client_name": "React Native OAuth Client Demo",
|
|
28
|
+
"client_uri": "https://example.com",
|
|
29
|
+
"redirect_uris": ["com.example:/auth/callback"],
|
|
30
|
+
"scope": "atproto repo:* rpc:*?aud=did:web:api.bsky.app#bsky_appview",
|
|
31
|
+
"token_endpoint_auth_method": "none",
|
|
32
|
+
"response_types": ["code"],
|
|
33
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
34
|
+
"application_type": "native",
|
|
35
|
+
"dpop_bound_access_tokens": true
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- The `client_id` should be the same URL as where you are serving your
|
|
40
|
+
`oauth-client-metadata.json` from
|
|
41
|
+
- The `client_uri` can be the home page of where you are serving your metadata
|
|
42
|
+
from
|
|
43
|
+
- Your `redirect_uris` should contain a native redirect URI (for ios/android),
|
|
44
|
+
as well as a web redirect URI (for web).
|
|
45
|
+
- native redirect URI must have a custom scheme, which is formatted as the
|
|
46
|
+
_reverse_ of the domain you are serving the metadata from. Since I am serving
|
|
47
|
+
mine from `example.com`, I use `com.example` as the scheme. If my domain were
|
|
48
|
+
`atproto.expo.dev`, I would use `dev.expo.atproto`. Additionally, the scheme
|
|
49
|
+
_must_ contain _only one trailing slash_ after the `:`. `com.example://` is
|
|
50
|
+
invalid.
|
|
51
|
+
- The `application_type` must be `native`
|
|
52
|
+
|
|
53
|
+
For a real-world example, see [Skylight's client metadata](https://skylight.expo.app/oauth/client-metadata.json).
|
|
54
|
+
|
|
55
|
+
For more information about client metadata, see [the Atproto documentation](https://atproto.com/specs/oauth#client-id-metadata-document).
|
|
56
|
+
|
|
57
|
+
### Create a client
|
|
58
|
+
|
|
59
|
+
Next, you want to create an `ExpoOAuthClient`. You will need to pass in the same client metadata to the client as you are serving in your `oauth-client-metadata.json`.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// utils/oauth-client.ts
|
|
63
|
+
const clientMetadata = require('../assets/oauth-client-metadata.json')
|
|
64
|
+
|
|
65
|
+
const client = new ExpoOAuthClient({
|
|
66
|
+
handleResolver: 'https://bsky.social',
|
|
67
|
+
clientMetadata,
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Sign a user in
|
|
72
|
+
|
|
73
|
+
Whenever you are ready, you can initiate a sign in attempt for the user using the client using `client.signIn(input)`
|
|
74
|
+
|
|
75
|
+
`input` must be one of the following:
|
|
76
|
+
|
|
77
|
+
- A valid Atproto user handle, e.g. `hailey.bsky.team` or `example.com`
|
|
78
|
+
- A valid DID, e.g. `did:web:example.com` or `did:plc:oisofpd7lj26yvgiivf3lxsi`
|
|
79
|
+
- A valid PDS host, e.g. `https://cocoon.example.com` or `https://bsky.social`
|
|
80
|
+
|
|
81
|
+
> [!NOTE] If you wish to allow a user to _create_ an account instead of signing
|
|
82
|
+
> in, simply use a valid PDS hostname rather than a handle. They will be
|
|
83
|
+
> presented the option to either Sign In with an existing account, or create a
|
|
84
|
+
> new one, on the PDS's sign in page.
|
|
85
|
+
|
|
86
|
+
The response of `signIn` will be a promise resolving to the following:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
| { status: WebBrowserResultType } // See Expo Web Browser documentation
|
|
90
|
+
| { status: 'error'; error: unknown }
|
|
91
|
+
| { status: 'success'; session: OAuthSession }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For example:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
try {
|
|
98
|
+
const session = await client.signIn(input ?? '')
|
|
99
|
+
setSession(session)
|
|
100
|
+
const agent = new Agent(session)
|
|
101
|
+
setAgent(agent)
|
|
102
|
+
} catch (err) {
|
|
103
|
+
Alert.alert('Error', String(err))
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Create an `Agent`
|
|
108
|
+
|
|
109
|
+
To interface with the various Atproto APIs, you will need to create an `Agent`. You will pass your `OAuthSession` to the `Agent` or `XrpcClient` constructor.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const agent = new Agent(session)
|
|
113
|
+
// or
|
|
114
|
+
const xrpc = new XrpcClient(session)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Session refreshes will be handled for you for the lifetime of the agent.
|
|
118
|
+
|
|
119
|
+
### Restoring a session
|
|
120
|
+
|
|
121
|
+
After, for example, closing the application, you will probably need to restore the user's session. You can do this by using the user's DID on the `ExpoOAuthClient`.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const session = await client.restore('did:plc:oisofpd7lj26yvgiivf3lxsi')
|
|
125
|
+
const agent = new Agent(session)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If the session needs to be refreshed, `.restore()` will automatically do this for you before returning a session (based on the token's expiration date). In order to force a refresh, you can pass in `true` as the second argument to `restore`.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const session = await client.restore(
|
|
132
|
+
'did:plc:oisofpd7lj26yvgiivf3lxsi',
|
|
133
|
+
true, // force a refresh, ensuring tokens were not revoked
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Additional Reading
|
|
138
|
+
|
|
139
|
+
- [Atproto OAuth Spec](https://atproto.com/specs/oauth)
|
|
140
|
+
- [Atproto Web OAuth Example](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-browser-example)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.atprotooauthclient'
|
|
4
|
+
version = '0.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
+
def useManagedAndroidSdkVersions = false
|
|
16
|
+
if (useManagedAndroidSdkVersions) {
|
|
17
|
+
useDefaultAndroidSdkVersions()
|
|
18
|
+
} else {
|
|
19
|
+
buildscript {
|
|
20
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
+
ext.safeExtGet = { prop, fallback ->
|
|
22
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
project.android {
|
|
26
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
29
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "expo.modules.atprotooauthclient"
|
|
36
|
+
defaultConfig {
|
|
37
|
+
versionCode 1
|
|
38
|
+
versionName "0.1.0"
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dependencies {
|
|
46
|
+
implementation "com.nimbusds:nimbus-jose-jwt:10.3.1"
|
|
47
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package expo.modules.atprotooauthclient
|
|
2
|
+
|
|
3
|
+
import com.nimbusds.jose.Algorithm
|
|
4
|
+
import com.nimbusds.jose.jwk.Curve
|
|
5
|
+
import com.nimbusds.jose.jwk.ECKey
|
|
6
|
+
import com.nimbusds.jose.jwk.KeyUse
|
|
7
|
+
import com.nimbusds.jose.util.Base64URL
|
|
8
|
+
import expo.modules.atprotooauthclient.EncodedJWK
|
|
9
|
+
import java.security.KeyPairGenerator
|
|
10
|
+
import java.security.MessageDigest
|
|
11
|
+
import java.security.interfaces.ECPrivateKey
|
|
12
|
+
import java.security.interfaces.ECPublicKey
|
|
13
|
+
import java.util.UUID
|
|
14
|
+
|
|
15
|
+
class Crypto {
|
|
16
|
+
fun digest(data: ByteArray): ByteArray {
|
|
17
|
+
val instance = MessageDigest.getInstance("sha256")
|
|
18
|
+
return instance.digest(data)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun getRandomValues(byteLength: Int): ByteArray {
|
|
22
|
+
val random = ByteArray(byteLength)
|
|
23
|
+
java.security.SecureRandom().nextBytes(random)
|
|
24
|
+
return random
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun generateJwk(): EncodedJWK {
|
|
28
|
+
val keyIdString = UUID.randomUUID().toString()
|
|
29
|
+
|
|
30
|
+
val keyPairGen = KeyPairGenerator.getInstance("EC")
|
|
31
|
+
keyPairGen.initialize(Curve.P_256.toECParameterSpec())
|
|
32
|
+
val keyPair = keyPairGen.generateKeyPair()
|
|
33
|
+
|
|
34
|
+
val publicKey = keyPair.public as ECPublicKey
|
|
35
|
+
val privateKey = keyPair.private as ECPrivateKey
|
|
36
|
+
|
|
37
|
+
val privateJwk =
|
|
38
|
+
ECKey
|
|
39
|
+
.Builder(Curve.P_256, publicKey)
|
|
40
|
+
.privateKey(privateKey)
|
|
41
|
+
.keyUse(KeyUse.SIGNATURE)
|
|
42
|
+
.keyID(keyIdString)
|
|
43
|
+
.algorithm(Algorithm.parse("ES256"))
|
|
44
|
+
.build()
|
|
45
|
+
|
|
46
|
+
return EncodedJWK().apply {
|
|
47
|
+
kty = privateJwk.keyType.value
|
|
48
|
+
crv = privateJwk.curve.toString()
|
|
49
|
+
kid = keyIdString
|
|
50
|
+
x = privateJwk.x.toString()
|
|
51
|
+
y = privateJwk.y.toString()
|
|
52
|
+
d = privateJwk.d.toString()
|
|
53
|
+
alg = privateJwk.algorithm.name
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fun decodeJwk(encodedJwk: EncodedJWK): ECKey {
|
|
58
|
+
val xb64url = Base64URL.from(encodedJwk.x)
|
|
59
|
+
val yb64url = Base64URL.from(encodedJwk.y)
|
|
60
|
+
val db64url = Base64URL.from(encodedJwk.d)
|
|
61
|
+
return ECKey
|
|
62
|
+
.Builder(Curve.P_256, xb64url, yb64url)
|
|
63
|
+
.d(db64url)
|
|
64
|
+
.keyUse(KeyUse.SIGNATURE)
|
|
65
|
+
.keyID(encodedJwk.kid)
|
|
66
|
+
.algorithm(Algorithm.parse(encodedJwk.alg))
|
|
67
|
+
.build()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package expo.modules.atprotooauthclient
|
|
2
|
+
|
|
3
|
+
import expo.modules.atprotooauthclient.Crypto
|
|
4
|
+
import expo.modules.atprotooauthclient.Jose
|
|
5
|
+
import expo.modules.kotlin.modules.Module
|
|
6
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
7
|
+
|
|
8
|
+
class ExpoAtprotoOAuthClientModule : Module() {
|
|
9
|
+
override fun definition() =
|
|
10
|
+
ModuleDefinition {
|
|
11
|
+
Name("ExpoAtprotoOAuthClient")
|
|
12
|
+
|
|
13
|
+
AsyncFunction("digest") { data: ByteArray, algo: String ->
|
|
14
|
+
if (algo != "sha256") {
|
|
15
|
+
throw IllegalArgumentException("Unsupported algorithm: $algo")
|
|
16
|
+
}
|
|
17
|
+
return@AsyncFunction Crypto().digest(data)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
AsyncFunction("getRandomValues") { byteLength: Int ->
|
|
21
|
+
return@AsyncFunction Crypto().getRandomValues(byteLength)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
AsyncFunction("generatePrivateJwk") { algo: String ->
|
|
25
|
+
if (algo != "ES256") {
|
|
26
|
+
throw IllegalArgumentException("Unsupported algorithm: $algo")
|
|
27
|
+
}
|
|
28
|
+
return@AsyncFunction Crypto().generateJwk()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
AsyncFunction("createJwt") { header: String, payload: String, encodedJwk: EncodedJWK ->
|
|
32
|
+
val jwk = Crypto().decodeJwk(encodedJwk)
|
|
33
|
+
return@AsyncFunction Jose().createJwt(header, payload, jwk)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
AsyncFunction("verifyJwt") { token: String, encodedJwk: EncodedJWK, options: VerifyOptions ->
|
|
37
|
+
val jwk = Crypto().decodeJwk(encodedJwk)
|
|
38
|
+
return@AsyncFunction Jose().verifyJwt(token, jwk, options)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
package expo.modules.atprotooauthclient
|
|
2
|
+
|
|
3
|
+
import com.nimbusds.jose.JWSHeader
|
|
4
|
+
import com.nimbusds.jose.crypto.ECDSASigner
|
|
5
|
+
import com.nimbusds.jose.crypto.ECDSAVerifier
|
|
6
|
+
import com.nimbusds.jose.jwk.ECKey
|
|
7
|
+
import com.nimbusds.jwt.JWTClaimsSet
|
|
8
|
+
import com.nimbusds.jwt.SignedJWT
|
|
9
|
+
import expo.modules.atprotooauthclient.VerifyOptions
|
|
10
|
+
import expo.modules.atprotooauthclient.VerifyResult
|
|
11
|
+
|
|
12
|
+
class InvalidPayloadException(
|
|
13
|
+
message: String,
|
|
14
|
+
) : Exception(message)
|
|
15
|
+
|
|
16
|
+
class Jose {
|
|
17
|
+
fun createJwt(
|
|
18
|
+
header: String,
|
|
19
|
+
payload: String,
|
|
20
|
+
jwk: ECKey,
|
|
21
|
+
): String {
|
|
22
|
+
val parsedHeader = JWSHeader.parse(header)
|
|
23
|
+
val parsedPayload = JWTClaimsSet.parse(payload)
|
|
24
|
+
|
|
25
|
+
val signer = ECDSASigner(jwk)
|
|
26
|
+
val jwt = SignedJWT(parsedHeader, parsedPayload)
|
|
27
|
+
jwt.sign(signer)
|
|
28
|
+
|
|
29
|
+
return jwt.serialize()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun verifyJwt(
|
|
33
|
+
token: String,
|
|
34
|
+
jwk: ECKey,
|
|
35
|
+
options: VerifyOptions,
|
|
36
|
+
): VerifyResult {
|
|
37
|
+
val jwt = SignedJWT.parse(token)
|
|
38
|
+
val verifier = ECDSAVerifier(jwk)
|
|
39
|
+
|
|
40
|
+
if (!jwt.verify(verifier)) {
|
|
41
|
+
throw InvalidPayloadException("invalid JWT signature")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
val protectedHeader = emptyMap<String, Any>().toMutableMap()
|
|
45
|
+
protectedHeader["alg"] = jwt.header.algorithm
|
|
46
|
+
|
|
47
|
+
jwt.header.getCustomParam("jku")?.let {
|
|
48
|
+
protectedHeader["jku"] = it.toString()
|
|
49
|
+
}
|
|
50
|
+
jwt.header.keyID?.let {
|
|
51
|
+
protectedHeader["kid"] = it
|
|
52
|
+
}
|
|
53
|
+
jwt.header.type?.let {
|
|
54
|
+
protectedHeader["typ"] = it.toString()
|
|
55
|
+
}
|
|
56
|
+
jwt.header.contentType?.let {
|
|
57
|
+
protectedHeader["cty"] = it
|
|
58
|
+
}
|
|
59
|
+
jwt.header.criticalParams?.let {
|
|
60
|
+
protectedHeader["crit"] = it.toList()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
options.typ?.let {
|
|
64
|
+
if (jwt.header.type.toString() != it) {
|
|
65
|
+
throw InvalidPayloadException("typ mismatch")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
val claims = jwt.jwtClaimsSet
|
|
70
|
+
|
|
71
|
+
options.requiredClaims?.let { requiredClaims ->
|
|
72
|
+
requiredClaims.forEach { claim ->
|
|
73
|
+
if (!claims.claims.containsKey(claim)) {
|
|
74
|
+
throw InvalidPayloadException("required claim '$claim' missing")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
options.audience?.let {
|
|
80
|
+
if (!claims.audience.contains(it)) {
|
|
81
|
+
throw InvalidPayloadException("audience mismatch")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
options.subject?.let {
|
|
86
|
+
if (claims.subject != it) {
|
|
87
|
+
throw InvalidPayloadException("subject mismatch")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
options.checkTolerance?.let {
|
|
92
|
+
val currentTime = options.currentDate ?: (System.currentTimeMillis() / 1000.0)
|
|
93
|
+
if (claims.issueTime.time / 1000.0 + it < currentTime) {
|
|
94
|
+
throw InvalidPayloadException("token expired")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
options.maxTokenAge?.let {
|
|
99
|
+
val currentTime = options.currentDate ?: (System.currentTimeMillis() / 1000.0)
|
|
100
|
+
if (claims.issueTime.time / 1000.0 + it < currentTime) {
|
|
101
|
+
throw InvalidPayloadException("token expired")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
options.issuer?.let {
|
|
106
|
+
if (claims.issuer != it) {
|
|
107
|
+
throw InvalidPayloadException("issuer mismatch")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return VerifyResult().apply {
|
|
112
|
+
payload = jwt.payload.toString()
|
|
113
|
+
this.protectedHeader = protectedHeader
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package expo.modules.atprotooauthclient
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.records.Field
|
|
4
|
+
import expo.modules.kotlin.records.Record
|
|
5
|
+
|
|
6
|
+
class EncodedJWK : Record {
|
|
7
|
+
@Field
|
|
8
|
+
var kty: String = ""
|
|
9
|
+
|
|
10
|
+
@Field
|
|
11
|
+
var crv: String = ""
|
|
12
|
+
|
|
13
|
+
@Field
|
|
14
|
+
var kid: String = ""
|
|
15
|
+
|
|
16
|
+
@Field
|
|
17
|
+
var x: String = ""
|
|
18
|
+
|
|
19
|
+
@Field
|
|
20
|
+
var y: String = ""
|
|
21
|
+
|
|
22
|
+
@Field
|
|
23
|
+
var d: String = ""
|
|
24
|
+
|
|
25
|
+
@Field
|
|
26
|
+
var alg: String = ""
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class VerifyOptions : Record {
|
|
30
|
+
@Field
|
|
31
|
+
var audience: String? = null
|
|
32
|
+
|
|
33
|
+
@Field
|
|
34
|
+
var checkTolerance: Double? = null
|
|
35
|
+
|
|
36
|
+
@Field
|
|
37
|
+
var issuer: String? = null
|
|
38
|
+
|
|
39
|
+
@Field
|
|
40
|
+
var maxTokenAge: Double? = null
|
|
41
|
+
|
|
42
|
+
@Field
|
|
43
|
+
var subject: String? = null
|
|
44
|
+
|
|
45
|
+
@Field
|
|
46
|
+
var typ: String? = null
|
|
47
|
+
|
|
48
|
+
@Field
|
|
49
|
+
var currentDate: Double? = null
|
|
50
|
+
|
|
51
|
+
@Field
|
|
52
|
+
var requiredClaims: Array<String>? = null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class VerifyResult : Record {
|
|
56
|
+
@Field
|
|
57
|
+
var payload: String = ""
|
|
58
|
+
|
|
59
|
+
@Field
|
|
60
|
+
var protectedHeader: Map<String, Any> = emptyMap()
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NativeModule } from 'expo';
|
|
2
|
+
import { SignedJwt, VerifyOptions, VerifyResult } from '@atproto/oauth-client';
|
|
3
|
+
import { ExpoAtprotoOAuthClientModuleEvents } from './ExpoAtprotoOAuthClientModule.types';
|
|
4
|
+
export type NativeJwk = {
|
|
5
|
+
kty: 'EC';
|
|
6
|
+
crv: 'P-256';
|
|
7
|
+
kid: string;
|
|
8
|
+
x: string;
|
|
9
|
+
y: string;
|
|
10
|
+
d: string;
|
|
11
|
+
alg: 'ES256';
|
|
12
|
+
};
|
|
13
|
+
declare class ExpoAtprotoOAuthClientModule extends NativeModule<ExpoAtprotoOAuthClientModuleEvents> {
|
|
14
|
+
digest(data: Uint8Array, algo: string): Promise<Uint8Array>;
|
|
15
|
+
getRandomValues(byteLength: number): Promise<Uint8Array>;
|
|
16
|
+
generatePrivateJwk(algorithm: string): Promise<NativeJwk>;
|
|
17
|
+
createJwt(header: string, payload: string, jwk: NativeJwk): Promise<SignedJwt>;
|
|
18
|
+
verifyJwt<C extends string = never>(token: SignedJwt, jwk: NativeJwk, options: VerifyOptions<C>): Promise<VerifyResult<C>>;
|
|
19
|
+
}
|
|
20
|
+
declare const _default: ExpoAtprotoOAuthClientModule;
|
|
21
|
+
export default _default;
|
|
22
|
+
//# sourceMappingURL=ExpoAtprotoOAuthClientModule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAtprotoOAuthClientModule.d.ts","sourceRoot":"","sources":["../src/ExpoAtprotoOAuthClientModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAC9E,OAAO,EAAE,kCAAkC,EAAE,MAAM,sCAAsC,CAAA;AAEzF,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,IAAI,CAAA;IACT,GAAG,EAAE,OAAO,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,GAAG,EAAE,OAAO,CAAA;CACb,CAAA;AAED,OAAO,OAAO,4BAA6B,SAAQ,YAAY,CAAC,kCAAkC,CAAC;IACjG,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAE3D,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAExD,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAEzD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IAE9E,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,KAAK,EAChC,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;CAC5B;;AAED,wBAEC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAtprotoOAuthClientModule.js","sourceRoot":"","sources":["../src/ExpoAtprotoOAuthClientModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAA;AA8BxD,eAAe,mBAAmB,CAChC,wBAAwB,CACzB,CAAA","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo'\nimport { SignedJwt, VerifyOptions, VerifyResult } from '@atproto/oauth-client'\nimport { ExpoAtprotoOAuthClientModuleEvents } from './ExpoAtprotoOAuthClientModule.types'\n\nexport type NativeJwk = {\n kty: 'EC'\n crv: 'P-256'\n kid: string\n x: string\n y: string\n d: string\n alg: 'ES256'\n}\n\ndeclare class ExpoAtprotoOAuthClientModule extends NativeModule<ExpoAtprotoOAuthClientModuleEvents> {\n digest(data: Uint8Array, algo: string): Promise<Uint8Array>\n\n getRandomValues(byteLength: number): Promise<Uint8Array>\n\n generatePrivateJwk(algorithm: string): Promise<NativeJwk>\n\n createJwt(header: string, payload: string, jwk: NativeJwk): Promise<SignedJwt>\n\n verifyJwt<C extends string = never>(\n token: SignedJwt,\n jwk: NativeJwk,\n options: VerifyOptions<C>,\n ): Promise<VerifyResult<C>>\n}\n\nexport default requireNativeModule<ExpoAtprotoOAuthClientModule>(\n 'ExpoAtprotoOAuthClient',\n)\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAtprotoOAuthClientModule.types.d.ts","sourceRoot":"","sources":["../src/ExpoAtprotoOAuthClientModule.types.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,kCAAkC,GAAG,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAtprotoOAuthClientModule.types.js","sourceRoot":"","sources":["../src/ExpoAtprotoOAuthClientModule.types.ts"],"names":[],"mappings":"","sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-types\nexport type ExpoAtprotoOAuthClientModuleEvents = {}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AuthorizeOptions, OAuthClient, OAuthSession } from '@atproto/oauth-client';
|
|
2
|
+
export interface ExpoOAuthClientInterface extends OAuthClient, Disposable {
|
|
3
|
+
signIn(input: string, options?: AuthorizeOptions): Promise<OAuthSession>;
|
|
4
|
+
handleCallback(): Promise<null | OAuthSession>;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=expo-oauth-client-interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo-oauth-client-interface.d.ts","sourceRoot":"","sources":["../src/expo-oauth-client-interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,YAAY,EACb,MAAM,uBAAuB,CAAA;AAE9B,MAAM,WAAW,wBAAyB,SAAQ,WAAW,EAAE,UAAU;IACvE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IACxE,cAAc,IAAI,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAA;CAC/C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo-oauth-client-interface.js","sourceRoot":"","sources":["../src/expo-oauth-client-interface.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n AuthorizeOptions,\n OAuthClient,\n OAuthSession,\n} from '@atproto/oauth-client'\n\nexport interface ExpoOAuthClientInterface extends OAuthClient, Disposable {\n signIn(input: string, options?: AuthorizeOptions): Promise<OAuthSession>\n handleCallback(): Promise<null | OAuthSession>\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OAuthClientMetadataInput, OAuthClientOptions, OAuthResponseMode } from '@atproto/oauth-client';
|
|
2
|
+
export type Simplify<T> = {
|
|
3
|
+
[K in keyof T]: T[K];
|
|
4
|
+
} & NonNullable<unknown>;
|
|
5
|
+
export type ExpoOAuthClientOptions = Simplify<{
|
|
6
|
+
clientMetadata: Readonly<OAuthClientMetadataInput>;
|
|
7
|
+
responseMode?: Exclude<OAuthResponseMode, 'form_post'>;
|
|
8
|
+
} & Omit<OAuthClientOptions, 'clientMetadata' | 'responseMode' | 'keyset' | 'runtimeImplementation' | 'sessionStore' | 'stateStore' | 'didCache' | 'handleCache' | 'dpopNonceCache' | 'authorizationServerMetadataCache' | 'protectedResourceMetadataCache'>>;
|
|
9
|
+
//# sourceMappingURL=expo-oauth-client-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo-oauth-client-options.d.ts","sourceRoot":"","sources":["../src/expo-oauth-client-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,uBAAuB,CAAA;AAE9B,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAC3C;IACE,cAAc,EAAE,QAAQ,CAAC,wBAAwB,CAAC,CAAA;IAClD,YAAY,CAAC,EAAE,OAAO,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAA;CACvD,GAAG,IAAI,CACN,kBAAkB,EAChB,gBAAgB,GAChB,cAAc,GACd,QAAQ,GACR,uBAAuB,GACvB,cAAc,GACd,YAAY,GACZ,UAAU,GACV,aAAa,GACb,gBAAgB,GAChB,kCAAkC,GAClC,gCAAgC,CACnC,CACF,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo-oauth-client-options.js","sourceRoot":"","sources":["../src/expo-oauth-client-options.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n OAuthClientMetadataInput,\n OAuthClientOptions,\n OAuthResponseMode,\n} from '@atproto/oauth-client'\n\nexport type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>\n\nexport type ExpoOAuthClientOptions = Simplify<\n {\n clientMetadata: Readonly<OAuthClientMetadataInput>\n responseMode?: Exclude<OAuthResponseMode, 'form_post'>\n } & Omit<\n OAuthClientOptions,\n | 'clientMetadata'\n | 'responseMode'\n | 'keyset'\n | 'runtimeImplementation'\n | 'sessionStore'\n | 'stateStore'\n | 'didCache'\n | 'handleCache'\n | 'dpopNonceCache'\n | 'authorizationServerMetadataCache'\n | 'protectedResourceMetadataCache'\n >\n>\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import './polyfill';
|
|
2
|
+
import { AuthorizeOptions, OAuthClient, OAuthSession } from '@atproto/oauth-client';
|
|
3
|
+
import { ExpoOAuthClientInterface } from './expo-oauth-client-interface';
|
|
4
|
+
import { ExpoOAuthClientOptions } from './expo-oauth-client-options';
|
|
5
|
+
export declare const CUSTOM_URI_SCHEME_REGEX: RegExp;
|
|
6
|
+
export declare class ExpoOAuthClient extends OAuthClient implements ExpoOAuthClientInterface {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(options: ExpoOAuthClientOptions);
|
|
9
|
+
handleCallback(): Promise<null | OAuthSession>;
|
|
10
|
+
signIn(input: string, options?: AuthorizeOptions): Promise<OAuthSession>;
|
|
11
|
+
[Symbol.dispose](): void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=expo-oauth-client.native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo-oauth-client.native.d.ts","sourceRoot":"","sources":["../src/expo-oauth-client.native.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAA;AAGnB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,YAAY,EACb,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAYpE,eAAO,MAAM,uBAAuB,QAA0C,CAAA;AAG9E,qBAAa,eACX,SAAQ,WACR,YAAW,wBAAwB;;gBAIvB,OAAO,EAAE,sBAAsB;IA8BrC,cAAc,IAAI,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC;IAI9C,MAAM,CACV,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,YAAY,CAAC;IAuCxB,CAAC,MAAM,CAAC,OAAO,CAAC;CAGjB"}
|