Help us understand the problem. What is going on with this article?

iOS独自アプリでの認証処理まとめ

More than 3 years have passed since last update.

ATSの必須化が予定されていたり、ますますセキュリティが厳しくなってきていますね。
今後は更にセキュリティレベルが高いアプリが求められることが予想されるため、しっかり理解しておきたいです。

iOS独自アプリからの認証処理を行う際にハマったのでまとめておきます。
検証した内容は以下の通りです。
①サーバの構築(Windows server 2012、 IIS 7.0)
②WEBページの作成(BASIC認証、SSL認証、クライアント認証を行うよう設定)
※SSL認証はオレオレ証明書を作成して使用
③iOSアプリから各種認証を行い、②のページにアクセス
今回は③の部分のみです。(①、②は既に良い記事があると思うので)

iOS独自アプリでの認証処理について

まず初めに、認証処理をする際に思ったのがiOSの証明書ストアに証明書を入れておけば勝手に認証してくれるんじゃ、と思って実装してみてもうまくいかない。
色々調べてみると、証明書ストアを参照できるのはsafari、メールのみという情報が。
独自アプリ(自分で作成したアプリ)は認証の際に自分で証明書ファイルを読み込んで処理してあげる必要があるようですね。

では、早速ですがサンプルコードを書いていきます。
まずはリクエストの作成です。簡単なものですが参考になれば。

サンプルコード

リクエストの作成
//NSURLSessionを使った通信処理
- (void)nsurlSessionClientCertificate
{
    NSString* url = @" URLを記述";
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]
                                             cachePolicy:NSURLRequestReloadIgnoringCacheData
                                     timeoutInterval:30.0];

    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                          delegate:self
                                                     delegateQueue:[NSOperationQueue mainQueue]];

    [[session dataTaskWithRequest: request  completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) {
        //レスポンスが成功か失敗かを見てそれぞれ処理を行う
        if (response && ! error)
        {
            NSString *responseString = [[NSString alloc] initWithData: data  encoding: NSShiftJISStringEncoding];
            NSLog(@"成功: %@", responseString);
        }
        else
        {
            NSLog(@"失敗: %@", error);
        }
    }] resume];
}

通常ですと、これでリクエストが送信されるのですが認証が必要な場合はデリゲートメソッドが呼ばれます。
当たり前ですが、デリゲートの設定を忘れずに。
認証の種類が取得できるので、種類によって処理を振り分けてあげる感じですね。
BASIC認証、SSL認証、クライアント認証が必要な場合この処理が3回呼ばれます。

認証処理
/** 処理概要:認証が必要な場合に呼び出される
 *
 */
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler
{
    //1度でも認証失敗している場合
    if ([challenge previousFailureCount] > 0) {
        //キャンセル処理
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
    else
    {
        //Basic認証
        if ( [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]
            || [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPDigest] )
        {
            NSURLCredential *credential = [[NSURLCredential alloc] initWithUser:@"ユーザID"
                                                                       password:@"パスワード"
                                                                    persistence:NSURLCredentialPersistenceForSession];

            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        }
        //SSL認証
        else if ( [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust] )
        {
            NSURLProtectionSpace *protecitionSpace = [challenge protectionSpace];
            SecTrustRef trust                      = [protecitionSpace serverTrust];
            NSURLCredential *credential            = [NSURLCredential credentialForTrust:trust];

            NSArray *certs = [[NSArray alloc] initWithObjects:(id)[[self class] sslCertificate], nil];

            OSStatus status = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)certs);
            if ( status != errSecSuccess )
            {
                //キャンセル処理
                completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
                return;
            }
            SecTrustResultType trustResult = kSecTrustResultInvalid;
            status = SecTrustEvaluate(trust, &trustResult);
            if ( status != errSecSuccess )
            {
                //キャンセル処理
                completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
                return;
            }

            switch ( trustResult )
            {
                case kSecTrustResultProceed:        // valid and user has explicitly accepted it.
                case kSecTrustResultUnspecified:    // valid and user has not explicitly accepted or reject it. generally you accept it in this case.
                {
                    //認証送信
                    completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
                    return;
                }
                    break;
                case kSecTrustResultRecoverableTrustFailure: // invalid, but in a way that may be acceptable, such as a name mismatch, expiration, or lack of trust (such as self-signed certificate)
                {
                    //キャンセル処理
                    [challenge.sender cancelAuthenticationChallenge:challenge];
                }
                    break;
                default:
                    //キャンセル処理
                    [challenge.sender cancelAuthenticationChallenge:challenge];
                    break;
            }
        }
        //クライアント認証
        else if ( [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate])
        {
            OSStatus status;
            CFArrayRef importedItems = NULL;

            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
            NSString *documentsDirPath = [paths objectAtIndex:0];
            NSString *pkcs12Path = [documentsDirPath stringByAppendingPathComponent:@"クライアント証明書ファイル名.pfx"];
            NSString *password = @"パスワード";

            //認証データP12のファイルを読み込み
            NSData *PKCS12Data = [NSData dataWithContentsOfFile:pkcs12Path];

            status = SecPKCS12Import((__bridge CFDataRef)PKCS12Data,
                                     (__bridge CFDictionaryRef) [NSDictionary dictionaryWithObjectsAndKeys:password,
                                                                 kSecImportExportPassphrase,
                                                                 nil],
                                     &importedItems);

            if (status == errSecSuccess) {

                NSArray* items = (__bridge NSArray*)importedItems;
                NSLog(@"items:%@", items);
                SecIdentityRef identityRef = (__bridge SecIdentityRef)[[items objectAtIndex:0] objectForKey:(__bridge id)kSecImportItemIdentity];
                NSURLCredential* credential = [NSURLCredential credentialWithIdentity:identityRef
                                                                         certificates:nil
                                                                          persistence:NSURLCredentialPersistenceNone];

                //認証送信
                completionHandler(NSURLSessionAuthChallengeUseCredential, credential);

                if (importedItems != NULL)
                    CFRelease(importedItems);
            }
        }
    }
}

+ (SecCertificateRef)sslCertificate
{
    if (!sslCertificate )
    {        
        //サーバー証明書はder形式でないと処理できない?
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"ファイル名" ofType:@"der"];
        NSData *data   = [[NSData alloc] initWithContentsOfFile:filePath];
        sslCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data);
    }

    return sslCertificate;
}

このメソッドでファイルを読み込んで認証処理をしてあげれば良いです。
なので、何かしらの手段でアプリ内に証明書を持たせる必要があります。
初めからアプリに組み込んでおくか、必要な時にサーバからダウンロードする等でしょうか。
ここで注意して欲しいのが、SSL認証の際に使用するファイルはder形式でないと正常に認証が行えませんでした。
オレオレ証明書を使う人は注意してください。
詳しくは下記の記事を参考に

RSA鍵、証明書のファイルフォーマットについて
windowsに取り込んでder形式で出力してもできそうです。
クライアント証明書はグローバサインさんのテスト証明書を使わせて頂きました。

以上となります。
思っていたより簡単にできましたのではないでしょうか。
この記事が誰かのお役に立てれば幸いです。

yosshi_0511
iOSアプリの開発をやってます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした