JWT Decode Dart

認証JWTDartセキュリティトークンデコード

ライブラリ

JWT Decode Dart

概要

JWT Decode Dartは、Dartアプリケーションで軽量なJSON Web Token(JWT)デコード機能を提供するライブラリです。

詳細

jwt_decodeは、Flutter/Dartアプリケーションでの認証トークン処理を簡素化するために設計された軽量ライブラリです。主要な機能として、JWTトークンのデコード、有効期限の確認、クレーム情報の抽出を提供します。このライブラリはトークンの検証は行わず、デコードに特化しているため、セキュリティクリティカルな用途では追加の検証手順が必要です。pub.devで公開されており、Dart 3に対応し、iOS、Android、Web、デスクトップの全プラットフォームで動作します。jwt_decoderという代替ライブラリも存在し、より多くのダウンロード実績を持ちますが、どちらも同様の機能を提供します。

メリット・デメリット

メリット

  • 軽量設計: 最小限の依存関係で高速なJWTデコード処理
  • クロスプラットフォーム: iOS、Android、Web、デスクトップ全対応
  • シンプルAPI: 直感的なメソッドで簡単にJWTデータを取得
  • 有効期限チェック: トークンの期限切れを自動判定
  • クレーム抽出: ペイロード内の任意のクレーム情報にアクセス
  • Flutter統合: Flutterアプリケーションとの親和性が高い

デメリット

  • 検証機能なし: トークンの署名検証は別途実装が必要
  • セキュリティ限定: デコードのみでセキュリティ機能は最小限
  • エラーハンドリング: 不正なトークンでの例外処理が必要
  • ドキュメント: 機能が限定的でドキュメントも最小限
  • 依存ライブラリ: 別途JWT検証ライブラリとの併用が必要

主要リンク

書き方の例

基本的なJWTデコード

import 'package:jwt_decode/jwt_decode.dart';

void main() {
  String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
  
  // JWTをデコード
  Map<String, dynamic> payload = Jwt.parseJwt(token);
  
  print('User ID: ${payload['sub']}');
  print('Email: ${payload['email']}');
  print('Role: ${payload['role']}');
}

有効期限の確認

import 'package:jwt_decode/jwt_decode.dart';

bool isTokenValid(String token) {
  try {
    // 有効期限をチェック
    bool isExpired = Jwt.isExpired(token);
    return !isExpired;
  } catch (e) {
    print('Invalid token: $e');
    return false;
  }
}

void checkToken() {
  String token = "your-jwt-token-here";
  
  if (isTokenValid(token)) {
    print('Token is valid');
    Map<String, dynamic> payload = Jwt.parseJwt(token);
    print('User: ${payload['name']}');
  } else {
    print('Token is expired or invalid');
  }
}

Flutter認証クラスでの使用

import 'package:jwt_decode/jwt_decode.dart';
import 'package:shared_preferences/shared_preferences.dart';

class AuthService {
  static const String _tokenKey = 'jwt_token';
  
  // トークンを保存
  Future<void> saveToken(String token) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_tokenKey, token);
  }
  
  // 保存されたトークンを取得・検証
  Future<Map<String, dynamic>?> getCurrentUser() async {
    final prefs = await SharedPreferences.getInstance();
    final token = prefs.getString(_tokenKey);
    
    if (token == null) return null;
    
    try {
      // 有効期限をチェック
      if (Jwt.isExpired(token)) {
        await logout(); // 期限切れの場合はログアウト
        return null;
      }
      
      // ユーザー情報を取得
      final payload = Jwt.parseJwt(token);
      return {
        'id': payload['sub'],
        'email': payload['email'],
        'name': payload['name'],
        'role': payload['role'],
      };
    } catch (e) {
      print('Token parsing error: $e');
      await logout();
      return null;
    }
  }
  
  // ログアウト
  Future<void> logout() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_tokenKey);
  }
  
  // 管理者権限チェック
  Future<bool> isAdmin() async {
    final user = await getCurrentUser();
    return user?['role'] == 'admin';
  }
}

エラーハンドリングとバリデーション

import 'package:jwt_decode/jwt_decode.dart';

class JwtUtil {
  static Map<String, dynamic>? safeParseJwt(String token) {
    try {
      return Jwt.parseJwt(token);
    } catch (e) {
      print('JWT parsing failed: $e');
      return null;
    }
  }
  
  static bool isValidJwt(String token) {
    if (token.isEmpty) return false;
    
    // JWTの基本形式チェック(3つの部分がドットで区切られている)
    final parts = token.split('.');
    if (parts.length != 3) return false;
    
    try {
      // デコード可能かチェック
      Jwt.parseJwt(token);
      return true;
    } catch (e) {
      return false;
    }
  }
  
  static DateTime? getExpirationDate(String token) {
    try {
      final payload = Jwt.parseJwt(token);
      final exp = payload['exp'] as int?;
      if (exp != null) {
        return DateTime.fromMillisecondsSinceEpoch(exp * 1000);
      }
    } catch (e) {
      print('Error getting expiration date: $e');
    }
    return null;
  }
  
  static Duration? getTimeUntilExpiration(String token) {
    final expDate = getExpirationDate(token);
    if (expDate != null) {
      return expDate.difference(DateTime.now());
    }
    return null;
  }
}

権限ベースのルーティング(Flutter)

import 'package:flutter/material.dart';
import 'package:jwt_decode/jwt_decode.dart';

class AuthGuard extends StatelessWidget {
  final Widget child;
  final String? requiredRole;
  final String? token;
  final Widget fallback;
  
  const AuthGuard({
    Key? key,
    required this.child,
    this.requiredRole,
    this.token,
    this.fallback = const Text('Access Denied'),
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    if (token == null || token!.isEmpty) {
      return fallback;
    }
    
    try {
      // トークンの有効期限チェック
      if (Jwt.isExpired(token!)) {
        return const Text('Token Expired');
      }
      
      // 必要な権限のチェック
      if (requiredRole != null) {
        final payload = Jwt.parseJwt(token!);
        final userRole = payload['role'] as String?;
        
        if (userRole != requiredRole) {
          return fallback;
        }
      }
      
      return child;
    } catch (e) {
      return Text('Authentication Error: $e');
    }
  }
}

// 使用例
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: AuthGuard(
        token: "your-jwt-token",
        requiredRole: "admin",
        child: AdminDashboard(),
        fallback: LoginScreen(),
      ),
    );
  }
}

リアルタイムトークン監視

import 'dart:async';
import 'package:jwt_decode/jwt_decode.dart';

class TokenMonitor {
  Timer? _expirationTimer;
  final Function() onTokenExpired;
  
  TokenMonitor({required this.onTokenExpired});
  
  void startMonitoring(String token) {
    stopMonitoring();
    
    try {
      final payload = Jwt.parseJwt(token);
      final exp = payload['exp'] as int?;
      
      if (exp != null) {
        final expirationDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
        final timeUntilExpiration = expirationDate.difference(DateTime.now());
        
        if (timeUntilExpiration.isNegative) {
          // 既に期限切れ
          onTokenExpired();
        } else {
          // 期限切れ直前にコールバックを実行
          _expirationTimer = Timer(timeUntilExpiration, onTokenExpired);
        }
      }
    } catch (e) {
      print('Error monitoring token: $e');
      onTokenExpired();
    }
  }
  
  void stopMonitoring() {
    _expirationTimer?.cancel();
    _expirationTimer = null;
  }
  
  void dispose() {
    stopMonitoring();
  }
}