C 言語にて libcurl を使って Gmail + OAuth で メールを送信する

C 言語にて libcurl を使って メールを送信する の続きです。

OAuth

OAuth は Gmail サーバが採用しているユーザ認証方式です。

クライアント側は、 アクセストークン (Access Token) を取得して、 下記の形式の応答を返す。

base64("user=" {User} "^Aauth=Bearer " {Access Token} "^AA")

OAuth 2.0 Mechanism https://developers.google.com/gmail/imap/xoauth2-protocol

OAuth の手順

OAuth2.0 を使用して GoogleAPI にアクセスする場合、すべてのアプリケーションは、次の5つのステップに従う。

(1) GoogleAPIコンソールからOAuth2.0 認証情報を取得する。
(2) Google認証サーバーから、OAuth2.0 認証情報を使って、リフレッッシュトークンとアクセストークンを取得する。
(3) ユーザーによって付与されたアクセスの範囲を調べる。
(4) アクセストークンをAPIに送信する。
(5) 必要に応じて、リフレッッシュトークンを使って、アクセストークンを更新する。

(2) のGoogle認証サーバーから、OAuth2.0 認証情報を使って、リフレッッシュトークンとアクセストークンを取得するときは、(3) のユーザによる認証が必要です。

(5) のフレッッシュトークンを使って、アクセストークンを更新するときは、 ユーザによる認証が不要です。

アクセストークンの有効期限は、1時間程度で短い。 リフレッッシュトークンは無期限。

そのため、クライアントアプリは、次のような手順をとる。
最初の1回目は、リフレッッシュトークンを取得する。
以降は、リフレッッシュトークンからアクセストークンを取得する。

Using OAuth 2.0 to Access Google APIs https://developers.google.com/identity/protocols/oauth2

Google にクライアントアプリを登録する

Google API コンソール にてアプリを登録して 認証情報 (credentials) を取得する。

Using OAuth 2.0 to Access Google APIs https://developers.google.com/identity/protocols/oauth2#2.-obtain-an-access-token-from-the-google-authorization-server.

Google API コンソール https://console.developers.google.com/

アプリを作成する

アプリは4つのステップで実行される。

(1) 認証コードを取得するためのURLを生成し、ユーザに表示する。

ユーザは、ウエブブラウザでURLにアクセスし、認証コード (Authorization Code) を取得する。 ユーザは、認証コードをアプリに入力する。

(2) Google 認証サーバーから、認証コードを使って、リフレッッシュトークンを取得する。

(3) Google 認証サーバーから、リフレッッシュトークンを使って、アクセストークン を取得する。

(4) Gmailサーバに、アクセストークンを使って、ログインする。

リフレッッシュトークンを取得する

下記を参考にした。
curl コマンドを使っているところを libcurl の http post で実装した。

Google Gmail APIでメールを取得する
https://qiita.com/ryurock/items/4b063372ede81780c3c8

下記の python コードを参考にした。

How-to: Send HTML Mails with OAuth2 and Gmail in Python
https://blog.macuyiko.com/post/2016/how-to-send-html-mails-with-oauth2-and-gmail-in-python.html

認証コードを使って、リフレッッシュトークンを取得する。

void get_refresh_token(void)
{
    readClientJsonFile( client_file, client_id, client_secret, error );

     buildRefreshTokenRequest(client_id, client_secret, auth_code, refresh_request, BUFSIZE);

    getRefreshTokenToFile(refresh_request, refresh_file, error, is_verbose);
}


bool  getRefreshTokenToFile(char * data, char * file, char *error, bool is_verbose)
{
    const char URL_TOKEN[] = 
    "https://accounts.google.com/o/oauth2/token";

    return http_post_to_file( (char *)URL_TOKEN, data, file, error, is_verbose);
}


bool http_post_to_file(char* url, char* postfileds,  char* file, char *error, bool is_verbose)
{
 
    FILE* fp;

  /* open the file */
    fp = fopen(file, "wb");
    if(!fp) {
        strcpy(error, "can not open: ");
        strcat(error, file);
        return false;
    }

    CURL *curl;
    CURLcode res;
    bool ret;

  /* In windows, this will init the winsock stuff */
    curl_global_init(CURL_GLOBAL_ALL);

  /* get a curl handle */
    curl = curl_easy_init();
    if(curl) {
        /* First set the URL that is about to receive our POST. This URL can
       just as well be a https:// URL if that is what should receive the
       data. */
        curl_easy_setopt(curl, CURLOPT_URL, url);

    /* Now specify the POST data */
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfileds);

  /* Switch on full protocol/debug output while testing */
        if (is_verbose){
            curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
        }

    /* write the page body to this file handle */
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);

  /* send all data to this function  */
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,  curl_file_write_data);

    /* Perform the request, res will get the return code */
        res = curl_easy_perform(curl);
    /* Check for errors */  
        if(res == CURLE_OK) {
            ret = true;
        } else{
            strcpy(error, curl_easy_strerror(res) );
            ret = false;
        }

        /* always cleanup */
        curl_easy_cleanup(curl);
    } // if(curl)

    curl_global_cleanup();

    fflush(fp);
    fclose(fp);

    return ret;
}

アクセストークン を取得する

リフレッッシュトークンを使って、アクセストークン を取得する。

void get_access_token(void)
{

    buildAccessTokenRequest( client_id, client_secret,  refresh_token, access_request, BUFSIZE );

    getAccessToken(access_request, access_response, error, is_verbose);
}

bool  getAccessToken(char * data, char * response, char *error, bool is_verbose)
{

    const char URL_TOKEN[] = 
    "https://accounts.google.com/o/oauth2/token";

    struct CurlMemory mem;

    bool ret = http_post_to_memory( (char *)URL_TOKEN, data, &mem, error, is_verbose);

    if(ret){
        strcpy(response , mem.memory);
    } 

    return ret;
}


bool http_post_to_memory(char* url, char* postfileds, struct CurlMemory* mem, char *error, bool is_verbose)
{

    CURL *curl;
    CURLcode res;
    bool ret;

  /* In windows, this will init the winsock stuff */
    curl_global_init(CURL_GLOBAL_ALL);

  /* get a curl handle */
    curl = curl_easy_init();
    if(curl) {
        /* First set the URL that is about to receive our POST. This URL can
       just as well be a https:// URL if that is what should receive the
       data. */
        curl_easy_setopt(curl, CURLOPT_URL, url);

    /* Now specify the POST data */
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfileds);

  /* Switch on full protocol/debug output while testing */
        if (is_verbose){
            curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
        }


  /* write data to a struct  */
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,  curl_mem_write_cb);
        initCurlMemory(mem);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, mem);


    /* Perform the request, res will get the return code */
        res = curl_easy_perform(curl);
    /* Check for errors */  
        if(res == CURLE_OK) {
            ret = true;
        } else{
            strcpy(error, curl_easy_strerror(res) );
            ret = false;
        }

        /* always cleanup */
        curl_easy_cleanup(curl);
    } // if(curl)

    curl_global_cleanup();

    return ret;
}

Gmailサーバに、OAuthを使って、ログインする。

libcurl は OAuth に対応している。

CURLOPT_XOAUTH2_BEARER を使用して、 前段で取得したアクセストークン を設定する

https://curl.haxx.se/libcurl/c/CURLOPT_XOAUTH2_BEARER.html

curl = curl_easy_init();
if(curl) {
    curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, access_token);
    curl_easy_setopt(curl, CURLOPT_USERNAME, user);
}

全体のコードは、githubに公開した。 https://github.com/ohwada/MAC_cpp_Samples/tree/master/libcurl/mail/xoauth

2022年 10月 追記

2022年 10月 より
Gmail および Google のサービスにおいて
OAUTH 認証にて WEBブラウザをリダイレクト先とする帯域外(OOB)フローが廃止になった。

(OOB)フロー移行ガイド https://developers.google.com/identity/protocols/oauth2/resources/oob-migration

上記のプログラムを実行すると下記の応答が返る。

リクエストは無効です

フロー移行の対応はこちらに。  C 言語にて libcurl と libmicrohttpd を使って Gmail + OAuth で メールを送信する