this repo has no description

add andrid

+1 -1
.eslintrc.js
···
// @generated by expo-module-scripts
-
module.exports = require('expo-module-scripts/eslintrc.base.js')
+
module.exports = require('expo-module-scripts/eslintrc.base.js');
+2
android/.editorconfig
···
+
[*.{kt,kts}]
+
indent_size=2
+4
android/build.gradle
···
abortOnError false
}
}
+
+
dependencies {
+
implementation "com.nimbusds:nimbus-jose-jwt:10.3.1"
+
}
+70
android/src/main/java/expo/modules/atprotoauth/Crypto.kt
···
+
package expo.modules.expoatprotoauth
+
+
import com.nimbusds.jose.Algorithm
+
import com.nimbusds.jose.jwk.Curve
+
import com.nimbusds.jose.jwk.ECKey
+
import com.nimbusds.jose.jwk.KeyUse
+
import com.nimbusds.jose.util.Base64URL
+
import expo.modules.atprotoauth.EncodedJWK
+
import java.security.KeyPairGenerator
+
import java.security.MessageDigest
+
import java.security.interfaces.ECPrivateKey
+
import java.security.interfaces.ECPublicKey
+
import java.util.UUID
+
+
class Crypto {
+
fun digest(data: ByteArray): ByteArray {
+
val instance = MessageDigest.getInstance("sha256")
+
return instance.digest(data)
+
}
+
+
fun getRandomValues(byteLength: Int): ByteArray {
+
val random = ByteArray(byteLength)
+
java.security.SecureRandom().nextBytes(random)
+
return random
+
}
+
+
fun generateJwk(): EncodedJWK {
+
val keyIdString = UUID.randomUUID().toString()
+
+
val keyPairGen = KeyPairGenerator.getInstance("EC")
+
keyPairGen.initialize(Curve.P_256.toECParameterSpec())
+
val keyPair = keyPairGen.generateKeyPair()
+
+
val publicKey = keyPair.public as ECPublicKey
+
val privateKey = keyPair.private as ECPrivateKey
+
+
val privateJwk =
+
ECKey
+
.Builder(Curve.P_256, publicKey)
+
.privateKey(privateKey)
+
.keyUse(KeyUse.SIGNATURE)
+
.keyID(keyIdString)
+
.algorithm(Algorithm.parse("ES256"))
+
.build()
+
+
return EncodedJWK().apply {
+
kty = privateJwk.keyType.value
+
use = "sig"
+
crv = privateJwk.curve.toString()
+
kid = keyIdString
+
x = privateJwk.x.toString()
+
y = privateJwk.y.toString()
+
d = privateJwk.d.toString()
+
alg = privateJwk.algorithm.name
+
}
+
}
+
+
fun decodeJwk(encodedJwk: EncodedJWK): ECKey {
+
val xb64url = Base64URL.from(encodedJwk.x)
+
val yb64url = Base64URL.from(encodedJwk.y)
+
val db64url = Base64URL.from(encodedJwk.d)
+
return ECKey
+
.Builder(Curve.P_256, xb64url, yb64url)
+
.d(db64url)
+
.keyUse(KeyUse.SIGNATURE)
+
.keyID(encodedJwk.kid)
+
.algorithm(Algorithm.parse(encodedJwk.alg))
+
.build()
+
}
+
}
+27 -36
android/src/main/java/expo/modules/atprotoauth/ExpoAtprotoAuthModule.kt
···
package expo.modules.atprotoauth
+
import expo.modules.expoatprotoauth.Crypto
+
import expo.modules.expoatprotoauth.Jose
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
-
import java.net.URL
class ExpoAtprotoAuthModule : Module() {
-
// Each module class must implement the definition function. The definition consists of components
-
// that describes the module's functionality and behavior.
-
// See https://docs.expo.dev/modules/module-api for more details about available components.
-
override fun definition() = ModuleDefinition {
-
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
-
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
-
// The module will be accessible from `requireNativeModule('ExpoAtprotoAuth')` in JavaScript.
-
Name("ExpoAtprotoAuth")
+
override fun definition() =
+
ModuleDefinition {
+
Name("ExpoAtprotoAuth")
-
// Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
-
Constants(
-
"PI" to Math.PI
-
)
+
Function("digest") { data: ByteArray, algo: String ->
+
if (algo != "sha256") {
+
throw IllegalArgumentException("Unsupported algorithm: $algo")
+
}
+
return@Function Crypto().digest(data)
+
}
-
// Defines event names that the module can send to JavaScript.
-
Events("onChange")
+
Function("getRandomValues") { byteLength: Int ->
+
return@Function Crypto().getRandomValues(byteLength)
+
}
-
// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
-
Function("hello") {
-
"Hello world! 👋"
-
}
+
Function("generatePrivateJwk") { algo: String ->
+
if (algo != "ES256") {
+
throw IllegalArgumentException("Unsupported algorithm: $algo")
+
}
+
return@Function Crypto().generateJwk()
+
}
-
// Defines a JavaScript function that always returns a Promise and whose native code
-
// is by default dispatched on the different thread than the JavaScript runtime runs on.
-
AsyncFunction("setValueAsync") { value: String ->
-
// Send an event to JavaScript.
-
sendEvent("onChange", mapOf(
-
"value" to value
-
))
-
}
+
Function("createJwt") { header: String, payload: String, encodedJwk: EncodedJWK ->
+
val jwk = Crypto().decodeJwk(encodedJwk)
+
return@Function Jose().createJwt(header, payload, jwk)
+
}
-
// Enables the module to be used as a native view. Definition components that are accepted as part of
-
// the view definition: Prop, Events.
-
View(ExpoAtprotoAuthView::class) {
-
// Defines a setter for the `url` prop.
-
Prop("url") { view: ExpoAtprotoAuthView, url: URL ->
-
view.webView.loadUrl(url.toString())
+
Function("verifyJwt") { token: String, encodedJwk: EncodedJWK, options: VerifyOptions ->
+
val jwk = Crypto().decodeJwk(encodedJwk)
+
return@Function Jose().verifyJwt(token, jwk, options)
}
-
// Defines an event that the view can send to JavaScript.
-
Events("onLoad")
}
-
}
}
-30
android/src/main/java/expo/modules/atprotoauth/ExpoAtprotoAuthView.kt
···
-
package expo.modules.atprotoauth
-
-
import android.content.Context
-
import android.webkit.WebView
-
import android.webkit.WebViewClient
-
import expo.modules.kotlin.AppContext
-
import expo.modules.kotlin.viewevent.EventDispatcher
-
import expo.modules.kotlin.views.ExpoView
-
-
class ExpoAtprotoAuthView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
-
// Creates and initializes an event dispatcher for the `onLoad` event.
-
// The name of the event is inferred from the value and needs to match the event name defined in the module.
-
private val onLoad by EventDispatcher()
-
-
// Defines a WebView that will be used as the root subview.
-
internal val webView = WebView(context).apply {
-
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
-
webViewClient = object : WebViewClient() {
-
override fun onPageFinished(view: WebView, url: String) {
-
// Sends an event to JavaScript. Triggers a callback defined on the view component in JavaScript.
-
onLoad(mapOf("url" to url))
-
}
-
}
-
}
-
-
init {
-
// Adds the WebView to the view hierarchy.
-
addView(webView)
-
}
-
}
+116
android/src/main/java/expo/modules/atprotoauth/Jose.kt
···
+
package expo.modules.expoatprotoauth
+
+
import com.nimbusds.jose.JWSHeader
+
import com.nimbusds.jose.crypto.ECDSASigner
+
import com.nimbusds.jose.crypto.ECDSAVerifier
+
import com.nimbusds.jose.jwk.ECKey
+
import com.nimbusds.jwt.JWTClaimsSet
+
import com.nimbusds.jwt.SignedJWT
+
import expo.modules.atprotoauth.VerifyOptions
+
import expo.modules.atprotoauth.VerifyResult
+
+
class InvalidPayloadException(
+
message: String,
+
) : Exception(message)
+
+
class Jose {
+
fun createJwt(
+
header: String,
+
payload: String,
+
jwk: ECKey,
+
): String {
+
val parsedHeader = JWSHeader.parse(header)
+
val parsedPayload = JWTClaimsSet.parse(payload)
+
+
val signer = ECDSASigner(jwk)
+
val jwt = SignedJWT(parsedHeader, parsedPayload)
+
jwt.sign(signer)
+
+
return jwt.serialize()
+
}
+
+
fun verifyJwt(
+
token: String,
+
jwk: ECKey,
+
options: VerifyOptions,
+
): VerifyResult {
+
val jwt = SignedJWT.parse(token)
+
val verifier = ECDSAVerifier(jwk)
+
+
if (!jwt.verify(verifier)) {
+
throw InvalidPayloadException("invalid JWT signature")
+
}
+
+
val protectedHeader = emptyMap<String, Any>().toMutableMap()
+
protectedHeader["alg"] = jwt.header.algorithm
+
+
jwt.header.getCustomParam("jku")?.let {
+
protectedHeader["jku"] = it.toString()
+
}
+
jwt.header.keyID?.let {
+
protectedHeader["kid"] = it
+
}
+
jwt.header.type?.let {
+
protectedHeader["typ"] = it.toString()
+
}
+
jwt.header.contentType?.let {
+
protectedHeader["cty"] = it
+
}
+
jwt.header.criticalParams?.let {
+
protectedHeader["crit"] = it.toList()
+
}
+
+
options.typ?.let {
+
if (jwt.header.type.toString() != it) {
+
throw InvalidPayloadException("typ mismatch")
+
}
+
}
+
+
val claims = jwt.jwtClaimsSet
+
+
options.requiredClaims?.let { requiredClaims ->
+
requiredClaims.forEach { claim ->
+
if (!claims.claims.containsKey(claim)) {
+
throw InvalidPayloadException("required claim '$claim' missing")
+
}
+
}
+
}
+
+
options.audience?.let {
+
if (!claims.audience.contains(it)) {
+
throw InvalidPayloadException("audience mismatch")
+
}
+
}
+
+
options.subject?.let {
+
if (claims.subject != it) {
+
throw InvalidPayloadException("subject mismatch")
+
}
+
}
+
+
options.checkTolerance?.let {
+
val currentTime = options.currentDate ?: (System.currentTimeMillis() / 1000.0)
+
if (claims.issueTime.time / 1000.0 + it < currentTime) {
+
throw InvalidPayloadException("token expired")
+
}
+
}
+
+
options.maxTokenAge?.let {
+
val currentTime = options.currentDate ?: (System.currentTimeMillis() / 1000.0)
+
if (claims.issueTime.time / 1000.0 + it < currentTime) {
+
throw InvalidPayloadException("token expired")
+
}
+
}
+
+
options.issuer?.let {
+
if (claims.issuer != it) {
+
throw InvalidPayloadException("issuer mismatch")
+
}
+
}
+
+
return VerifyResult().apply {
+
payload = jwt.payload.toString()
+
this.protectedHeader = protectedHeader
+
}
+
}
+
}
+64
android/src/main/java/expo/modules/atprotoauth/Records.kt
···
+
package expo.modules.atprotoauth
+
+
import expo.modules.kotlin.records.Field
+
import expo.modules.kotlin.records.Record
+
+
class EncodedJWK : Record {
+
@Field
+
var kty: String = ""
+
+
@Field
+
var use: String = ""
+
+
@Field
+
var crv: String = ""
+
+
@Field
+
var kid: String = ""
+
+
@Field
+
var x: String = ""
+
+
@Field
+
var y: String = ""
+
+
@Field
+
var d: String = ""
+
+
@Field
+
var alg: String = ""
+
}
+
+
class VerifyOptions : Record {
+
@Field
+
var audience: String? = null
+
+
@Field
+
var checkTolerance: Double? = null
+
+
@Field
+
var issuer: String? = null
+
+
@Field
+
var maxTokenAge: Double? = null
+
+
@Field
+
var subject: String? = null
+
+
@Field
+
var typ: String? = null
+
+
@Field
+
var currentDate: Double? = null
+
+
@Field
+
var requiredClaims: Array<String>? = null
+
}
+
+
class VerifyResult : Record {
+
@Field
+
var payload: String = ""
+
+
@Field
+
var protectedHeader: Map<String, Any> = emptyMap()
+
}
+21 -28
example/App.tsx
···
import React from 'react'
-
import { Text, View, StyleSheet, Button, Alert, TextInput } from 'react-native'
+
import {
+
Text,
+
View,
+
StyleSheet,
+
Button,
+
Alert,
+
TextInput,
+
Platform,
+
} from 'react-native'
import {
digest,
getRandomValues,
···
import { OAuthSession } from '@atproto/oauth-client'
import { Agent } from '@atproto/api'
import type { ReactNativeKey } from 'expo-atproto-auth'
-
import * as Browser from 'expo-web-browser'
const client = new ReactNativeOAuthClient({
clientMetadata: {
···
<Button
title="Open Sign In"
onPress={async () => {
-
let url: URL
-
try {
-
url = await client.authorize(input ?? '')
-
} catch (e: any) {
-
Alert.alert('Error', e.toString())
-
return
-
}
-
const res = await Browser.openAuthSessionAsync(
-
url.toString(),
-
'at.hailey://auth/callback'
-
)
-
-
if (res.type === 'success') {
-
const resUrl = new URL(res.url)
-
try {
-
const params = new URLSearchParams(resUrl.hash.substring(1))
-
const callbackRes = await client.callback(params)
-
setSession(callbackRes.session)
-
-
const newAgent = new Agent(callbackRes.session)
-
setAgent(newAgent)
-
} catch (e: any) {
-
Alert.alert('Error', e.toString())
-
}
+
const res = await client.signIn(input ?? '')
+
if (res.status === 'success') {
+
setSession(res.session)
+
const newAgent = new Agent(res.session)
+
setAgent(newAgent)
+
} else if (res.status === 'error') {
+
Alert.alert('Error', (res.error as any).toString())
} else {
-
Alert.alert('Error', `Received non-success status: ${res.type}`)
+
Alert.alert(
+
'Error',
+
`Received unknown WebResultType: ${res.status}`
+
)
}
}}
/>
···
onPress={async () => {
try {
await agent?.post({
-
text: 'Test post from Expo Atproto Auth example',
+
text: `Test post from Expo Atproto Auth example using platform ${Platform.OS}`,
})
} catch (e: any) {
Alert.alert('Error', e.toString())
+2 -2
example/ios/expoatprotoauthexample.xcodeproj/project.pbxproj
···
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = expo.modules.atprotoauth.example;
-
PRODUCT_NAME = expoatprotoauthexample;
+
PRODUCT_NAME = "expoatprotoauthexample";
SWIFT_OBJC_BRIDGING_HEADER = "expoatprotoauthexample/expoatprotoauthexample-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
···
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = expo.modules.atprotoauth.example;
-
PRODUCT_NAME = expoatprotoauthexample;
+
PRODUCT_NAME = "expoatprotoauthexample";
SWIFT_OBJC_BRIDGING_HEADER = "expoatprotoauthexample/expoatprotoauthexample-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
+3 -3
ios/Crypto.swift
···
return Data(bytes)
}
-
static func generateJwk() -> JWK {
+
static func generateJwk() -> EncodedJWK {
let kid = UUID().uuidString
let privKey = P256.Signing.PrivateKey()
···
let y = pubKey.x963Representation[33...].base64URLEncodedString()
let d = privKey.rawRepresentation.base64URLEncodedString()
-
let jwk = JWK()
+
let jwk = EncodedJWK()
jwk.kty = "EC"
jwk.use = "sig"
jwk.crv = "P-256"
···
return jwk
}
-
static func importJwk(x: String, y: String, d: String) throws -> SecKey {
+
static func decodeJwk(x: String, y: String, d: String) throws -> SecKey {
func base64UrlDecode(_ string: String) -> Data? {
var base64 = string
.replacingOccurrences(of: "-", with: "+")
+7 -13
ios/ExpoAtprotoAuthModule.swift
···
return CryptoUtil.getRandomValues(byteLength: byteLength)
}
-
Function("generatePrivateJwk") { (algo: String) throws -> JWK in
+
Function("generatePrivateJwk") { (algo: String) throws -> EncodedJWK in
if algo != "ES256" {
throw ExpoAtprotoAuthError.unsupportedAlgorithm(algo)
}
return CryptoUtil.generateJwk()
}
-
Function("createJwt") { (header: String, payload: String, jwk: JWK) throws -> String in
-
let key = try CryptoUtil.importJwk(x: jwk.x, y: jwk.y, d: jwk.d)
-
let jwt = try JoseUtil.createJwt(header: header, payload: payload, jwk: key)
+
Function("createJwt") { (header: String, payload: String, jwk: EncodedJWK) throws -> String in
+
let jwk = try CryptoUtil.decodeJwk(x: jwk.x, y: jwk.y, d: jwk.d)
+
let jwt = try JoseUtil.createJwt(header: header, payload: payload, jwk: jwk)
return jwt
}
-
Function("verifyJwt") { (token: String, jwk: JWK, options: VerifyOptions) throws -> VerifyResponse in
-
let key = try CryptoUtil.importJwk(x: jwk.x, y: jwk.y, d: jwk.d)
-
let res = try JoseUtil.verifyJwt(token: token, jwk: key, options: options)
+
Function("verifyJwt") { (token: String, jwk: EncodedJWK, options: VerifyOptions) throws -> VerifyResult in
+
let jwk = try CryptoUtil.decodeJwk(x: jwk.x, y: jwk.y, d: jwk.d)
+
let res = try JoseUtil.verifyJwt(token: token, jwk: jwk, options: options)
return res
-
}
-
-
AsyncFunction("setValueAsync") { (value: String) in
-
self.sendEvent("onChange", [
-
"value": value
-
])
}
}
}
+2 -2
ios/Jose.swift
···
return jws.compactSerializedString
}
-
static func verifyJwt(token: String, jwk: SecKey, options: VerifyOptions) throws -> VerifyResponse {
+
static func verifyJwt(token: String, jwk: SecKey, options: VerifyOptions) throws -> VerifyResult {
guard let jws = try? JWS(compactSerialization: token),
let verifier = Verifier(verifyingAlgorithm: .ES256, key: jwk),
let validation = try? jws.validate(using: verifier)
···
}
}
-
let res = VerifyResponse()
+
let res = VerifyResult()
res.payload = payload
res.protectedHeader = protectedHeader
+2 -2
ios/Records.swift
···
import ExpoModulesCore
-
struct JWK: Record {
+
struct EncodedJWK: Record {
@Field
var kty: String
···
var requiredClaims: [String]?
}
-
struct VerifyResponse: Record {
+
struct VerifyResult: Record {
@Field
var payload: String
+3 -1
package.json
···
"build": "expo-module build",
"clean": "expo-module clean",
"typecheck": "tsc",
-
"lint": "eslint \"**/*.{js,ts,tsx}\"",
+
"lint": "eslint \"**/*.{ts,tsx}\"",
"test": "expo-module test",
"prepare": "expo-module prepare",
"prepublishOnly": "expo-module prepublishOnly",
···
"eslint-plugin-prettier": "^5.2.3",
"expo": "~53.0.0",
"expo-module-scripts": "^4.1.9",
+
"expo-web-browser": "^14.2.0",
"jest": "^29.7.0",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
···
"peerDependencies": {
"@atproto/oauth-client": "*",
"expo": "*",
+
"expo-web-browser": "*",
"react": "*",
"react-native": "*",
"react-native-mmkv": "*"
+1 -1
src/ExpoAtprotoAuth.types.ts
···
requiredClaims?: string[]
}
-
export type VerifyResponse = {
+
export type VerifyResult = {
payload: string
protectedHeader: JwtHeader
}
+1 -1
src/index.ts
···
verifyJwt,
ReactNativeKey,
} from './react-native-key'
-
export { ReactNativeOAuthClient } from './react-native-oauth-client'
+
export { ExpoOAuthClient as ReactNativeOAuthClient } from './react-native-oauth-client'
+35 -1
src/react-native-oauth-client.ts
···
type OAuthResponseMode,
atprotoLoopbackClientMetadata,
OAuthClient,
+
OAuthSession,
} from '@atproto/oauth-client'
import { ReactNativeRuntimeImplementation } from './react-native-runtime-implementation'
import { ReactNativeOAuthDatabase } from './react-native-oauth-database'
+
import { openAuthSessionAsync, WebBrowserResultType } from 'expo-web-browser'
export type Simplify<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>
···
>
>
-
export class ReactNativeOAuthClient extends OAuthClient {
+
export class ExpoOAuthClient extends OAuthClient {
constructor({
responseMode = 'fragment',
...options
···
protectedResourceMetadataCache:
database.getProtectedResourceMetadataCache(),
})
+
}
+
+
async signIn(
+
input: string
+
): Promise<
+
| { status: WebBrowserResultType }
+
| { status: 'error'; error: unknown }
+
| { status: 'success'; session: OAuthSession }
+
> {
+
let url: URL
+
try {
+
url = await this.authorize(input)
+
} catch (e: unknown) {
+
return { status: 'error', error: e }
+
}
+
+
const res = await openAuthSessionAsync(
+
url.toString(),
+
this.clientMetadata.redirect_uris[0],
+
{
+
createTask: false,
+
}
+
)
+
+
if (res.type === 'success') {
+
const resUrl = new URL(res.url)
+
const params = new URLSearchParams(resUrl.hash.substring(1))
+
const callbackRes = await this.callback(params)
+
return { status: 'success', session: callbackRes.session }
+
} else {
+
return { status: res.type }
+
}
}
}
+5
yarn.lock
···
dependencies:
invariant "^2.2.4"
+
expo-web-browser@^14.2.0:
+
version "14.2.0"
+
resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-14.2.0.tgz#d8fb521ae349aebbf5c0ca32448877480124c06c"
+
integrity sha512-6S51d8pVlDRDsgGAp8BPpwnxtyKiMWEFdezNz+5jVIyT+ctReW42uxnjRgtsdn5sXaqzhaX+Tzk/CWaKCyC0hw==
+
expo@~53.0.0:
version "53.0.19"
resolved "https://registry.yarnpkg.com/expo/-/expo-53.0.19.tgz#69f8ed224efadf517d3ff66dde3d536fb3e8f00a"