Skip to main content

ghayma_auth 0.3.0 copy "ghayma_auth: ^0.3.0" to clipboard
ghayma_auth: ^0.3.0 copied to clipboard

Client for the Ghayma auth service: sign-in, sessions, 2FA, PKCE OAuth and native Google sign-in for Dart and Flutter apps.

ghayma_auth #

Dart client for the Ghayma auth service: email/password sign-in, sessions with automatic refresh, TOTP two-factor, profile management, PKCE OAuth and native Google sign-in.

Pure Dart — http and crypto only, no Flutter dependency — so the same client runs in a Flutter app, a CLI and a server. Flutter integrates through two seams: a TokenStorage adapter for persistence, and handing the OAuth redirect URI to handleRedirect.

Install #

dart pub add ghayma_auth

Quick start #

import 'package:ghayma_auth/ghayma_auth.dart';

final auth = GhaymaAuth(appSlug: 'my-app');

await auth.init(); // restore a persisted session

final result = await auth.login(
  email: 'user@example.com',
  password: 's3cret-passphrase',
);

switch (result) {
  case LoginSuccess(:final session):
    print('signed in as ${session.user.email}');
  case TwoFaRequired(:final challengeToken):
    await auth.verify2fa(challengeToken: challengeToken, code: '123456');
  case TwoFaEnrollmentRequired(:final enrollToken):
    final enrollment = await auth.enrollTotp(enrollToken: enrollToken);
    final confirmation = await auth.confirmTotp(
      code: '123456',
      enrollToken: enrollToken,
    );
    print('recovery codes: ${confirmation.recoveryCodes}');
}

final user = await auth.getUser();
await auth.logout();
auth.dispose();

register answers the same way: RegisterSuccess when tokens are issued, VerificationRequired when the app requires a verified email first.

See example/main.dart for a runnable walkthrough.

Flutter integration #

Persist the session #

The default InMemoryTokenStorage forgets everything when the process exits. Implement TokenStorage over the store of your choice — here flutter_secure_storage:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:ghayma_auth/ghayma_auth.dart';

class SecureTokenStorage implements TokenStorage {
  static const _key = 'ghayma_session';
  final _storage = const FlutterSecureStorage();

  @override
  Future<String?> read() => _storage.read(key: _key);

  @override
  Future<void> write(String value) => _storage.write(key: _key, value: value);

  @override
  Future<void> delete() => _storage.delete(key: _key);
}

final auth = GhaymaAuth(appSlug: 'my-app', storage: SecureTokenStorage());

Startup and routing #

Call init() once before the first frame and route on onAuthStateChange afterwards:

await auth.init();
runApp(MyApp(signedIn: auth.isAuthenticated));

auth.onAuthStateChange.listen((state) {
  switch (state.event) {
    case AuthEvent.signedIn:
      router.go('/home');
    case AuthEvent.signedOut:
      router.go('/login');
    case AuthEvent.tokenRefreshed:
    case AuthEvent.userUpdated:
      break;
  }
});

getAccessToken() refreshes when the token is within 30 s of expiry, and a background timer rotates it 60 s before it lapses, so calling it before each API request is enough.

OAuth in a browser tab #

PKCE only: no token ever travels through the OS URL handler. Open the URL with flutter_web_auth_2 5.x and hand the callback straight back:

final start = await auth.startOAuth(
  OAuthProvider.google,
  redirectUri: 'com.example.app://callback',
);

final result = await FlutterWebAuth2.authenticate(
  url: start.url,
  callbackUrlScheme: 'com.example.app',
);

await auth.handleRedirect(Uri.parse(result));

startOAuth keeps the verifier in memory and also returns it in OAuthStart.codeVerifier, so an app that hands off to an external browser can persist it and pass it back: handleRedirect(uri, codeVerifier: saved).

Native Google sign-in #

With google_sign_in 7.x the platform hands you an ID token and no browser is involved:

final account = await GoogleSignIn.instance.authenticate();
final idToken = account.authentication.idToken;

if (idToken != null) {
  await auth.signInWithIdToken(idToken: idToken);
}

Second factor on OAuth sign-in #

When the app's 2FA policy applies to the user, handleRedirect, exchangeCode and signInWithIdToken store no session and throw TwoFactorRequiredException. Its result is the pending step login returns for the same user, and the sign-in finishes the same way:

try {
  await auth.handleRedirect(Uri.parse(result));
} on TwoFactorRequiredException catch (e) {
  if (e.result case TwoFaRequired(:final challengeToken)) {
    await auth.verify2fa(challengeToken: challengeToken, code: '123456');
  } else if (e.result case TwoFaEnrollmentRequired(:final enrollToken)) {
    final enrollment = await auth.enrollTotp(enrollToken: enrollToken);
    print('scan ${enrollment.otpauthUri} in an authenticator');
    await auth.confirmTotp(code: '123456', enrollToken: enrollToken);
  }
}

It extends GhaymaAuthException, so catch it first when you catch both.

Console setup #

In the Ghayma console, for your auth app:

  • Allowed Origins must list the deep link you pass as redirectUri (com.example.app://callback), exactly as written.
  • Native client IDs must list the iOS and Android OAuth client ids you use with google_sign_in; the service verifies the ID token against them.

Full walkthrough: https://docs.ghayma.cloud/guides/oauth-mobile.

Server usage #

On a Dart server, pass the app's server key (ghs_…) and forward the end user's IP so rate limits are charged to that address rather than to your server:

final auth = GhaymaAuth(
  appSlug: 'my-app',
  serverKey: Platform.environment['GHAYMA_AUTH_SERVER_KEY'],
);

await auth.login(
  email: email,
  password: password,
  options: RequestOptions(clientIp: request.headers.value('x-real-ip')),
);

On Ghayma, request.remoteAddress is the platform's edge; the visitor's IP is in X-Real-IP. X-Real-IP is trustworthy because Ghayma's edge overwrites it; if your server is also reachable another way (another host, your own proxy), make sure that path overwrites it too, or don't forward it.

The two headers travel together or not at all: without a serverKey the clientIp is dropped, and anything that is not a bare IP literal (a whole x-forwarded-for chain, for instance) is dropped too. Never ship a server key to a client app.

Errors #

Every failure is a GhaymaAuthException with a status, a code, a message and, on rate limits, retryAfter in seconds.

code When
invalid_request the request was malformed or a field was rejected
invalid_grant a spent, expired or mismatched one-time code at exchangeCode
invalid_token a provider ID token that failed verification
email_not_verified a Google ID token for an address Google has not verified (403)
rate_limited 429; read retryAfter
oauth_error the provider handed back ?error= on the redirect
two_fa_required a TwoFactorRequiredException: an OAuth sign-in awaits a 2FA code (status 200)
two_fa_enrollment_required a TwoFactorRequiredException: an OAuth sign-in awaits TOTP enrolment (status 200)
network_error the request never reached the service (status 0)
timeout no answer within 30 s (status 408)
auth_error anything the service did not label — a wrong password or 2FA code is a plain 401 here, a missing session is 401 before any request
try {
  await auth.login(email: email, password: password);
} on GhaymaAuthException catch (err) {
  if (err.code == 'rate_limited') {
    print('try again in ${err.retryAfter} s');
  }
}

A refresh the service rejects clears the session and emits AuthEvent.signedOut before the error reaches you.

Contract and conformance #

Every request body, response field and error string in this package comes from the published contract, vendored at spec/auth.v1.yaml and refreshed with tool/sync_spec.sh from https://auth.ghayma.tech/openapi.yaml. CI fails when the copy is stale.

Two test layers:

dart test              # unit tests against a mock transport
tool/conformance.sh    # every method against a Prism mock of the contract

The conformance suite starts @stoplight/prism-cli on the vendored spec with --errors, so an off-contract request fails the run rather than passing silently. It needs Node 22 with npx.

Development #

dart pub get
dart format --output=none --set-exit-if-changed .
dart analyze --fatal-infos
dart test
tool/conformance.sh

License #

MIT — see LICENSE.

0
likes
160
points
154
downloads

Documentation

API reference

Publisher

verified publisherghayma.tech

Weekly Downloads

Client for the Ghayma auth service: sign-in, sessions, 2FA, PKCE OAuth and native Google sign-in for Dart and Flutter apps.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

crypto, http

More

Packages that depend on ghayma_auth