由於前陣子客戶那邊的IMAP服務突然不會運作了,查了一下問題竟然是登入失敗?WTF,明明什麼都沒有動(那個IMAP服務大概有兩三年沒有維護了)突然間就無法登入加上客戶急的跳腳,緊急之下就開始爬文後來才知道微軟把基本驗證(就是在Client端直接輸入帳號密碼的方式)給關掉了【點我前往參考】
上面的鏈接是說直接輸入帳號密碼的方式不安全,而微軟要我們直接使用OAuth驗證的方式,OK又是一個新的挑戰讓我們開始Delphi - OAuth的世界吧!
元件準備
首先我們需要準備這幾個元件
- TsgcHTTP_OAuth2_Client
- TIdSSLIOHandlerSocketOpenSSL
- TIdIMAP4
procedure TForm1.btn_Authorization_Code_FlowClick(Sender: TObject);
begin
DoLog('Start Authorization Code Flow');
OAuth2_Authorization_Code.Start;
end;
在Get IMAP - Button的OnClick事件實作IMAP的連線以及取得指定MailBox裡面的的Email數量
try
try
if TIdSASLXOAuth(xOAuthSASL.SASL).Token <> '' then begin
if not TIdSASLXOAuth(xOAuthSASL.SASL).IsTokenExpired then begin
DoLog('Start Connect Outlook');
IdIMAP4.Connect;
DoLog('Connected Outlook');
IdIMAP4.SelectMailBox('INBOX');
DoLog('Your Outlook TotalMsgs: ' + IdIMAP4.MailBox.TotalMsgs.ToString);
IdIMAP4.Disconnect;
DoLog('Disconnected Outlook');
end else begin
DoLog('Access Token is expired!!');
end;
end else begin
DoLog('Access Token is empty!!');
end;
except
on E: Exception do begin
DoLog('IMAP Exception: ' + E.ToString);
end;
end;
finally
if TIdSASLXOAuth(xOAuthSASL.SASL).IsTokenExpired then TIdSASLXOAuth(xOAuthSASL.SASL).Token := '';
end;
實現TIdIMAP4的SASL XOAuth驗證機制功能
客戶的收信服務本身是使用TIdIMAP4這個元件為了讓修改幅度降到最低因此選擇繼續沿用TIdIMAP4,但是由於TIdIMAP4本身沒有連OAuth的功能因此要自己另外實作,實作前IdIMAP4需要先設定幾個屬性
- AuthType:這裡需要調整為iatSASL,預設是iatUserPass但由於大多的IMAP都開始不支持Auth Login的方式了因此未來應該都會直接使用iatSASL的方式
- Host:這裡就直接填入Outlook的IMAP Server Host
- Port:Outlook的IMAP Server Port
- UseTLS:這裡直接使用utUseImplicitTLS(隱式TLS)
接著要實作SASL的XOAuth驗證方式,這裡我就直接參考別人寫好的TIdSASLXOAuth【點我前往Github參考範例】
unit IdSASLXOAUTH;
interface
uses
System.SysUtils,
System.DateUtils,
Classes,
IdSASL
;
type
TIdSASLXOAuth = class(TIdSASL)
private
FToken: string;
FUser: string;
FExpireTime: string;
FGetTokenDateTime: TDateTime;
procedure GetToken(const Value: string);
public
property Token: string read FToken write GetToken;
property User: string read FUser write FUser;
property ExpireTime: string read FExpireTime write FExpireTime;
class function ServiceName: TIdSASLServiceName; override;
constructor Create(AOwner: TComponent);
destructor Destroy; override;
function TryStartAuthenticate(const AHost, AProtocolName : String; var VInitialResponse: String): Boolean; override;
function ContinueAuthenticate(const ALastResponse, AHost, AProtocolName : string): string; override;
function StartAuthenticate(const AChallenge, AHost, AProtocolName: string): string; override;
{ For cleaning up after Authentication }
procedure FinishAuthenticate; override;
function IsTokenExpired: Boolean;
end;
implementation
{ TIdSASLXOAuth }
class function TIdSASLXOAuth.ServiceName: TIdSASLServiceName;
begin
Result := 'XOAUTH2';
end;
constructor TIdSASLXOAuth.Create(AOwner: TComponent);
begin
inherited;
FExpireTime := '3599';
end;
destructor TIdSASLXOAuth.Destroy;
begin
inherited;
end;
function TIdSASLXOAuth.TryStartAuthenticate(const AHost, AProtocolName: String; var VInitialResponse: String): Boolean;
begin
VInitialResponse := 'user=' + FUser + Chr($01) + 'auth=Bearer ' + FToken + Chr($01) + Chr($01);
Result := True;
end;
function TIdSASLXOAuth.StartAuthenticate(const AChallenge, AHost, AProtocolName: string): string;
begin
Result := 'user=' + FUser + Chr($01) + 'auth=Bearer ' + FToken + Chr($01) + Chr($01);
end;
function TIdSASLXOAuth.ContinueAuthenticate(const ALastResponse, AHost, AProtocolName: string): string;
begin
// Nothing to do
end;
procedure TIdSASLXOAuth.FinishAuthenticate;
begin
// Nothing to do
end;
procedure TIdSASLXOAuth.GetToken(const Value: string);
begin
FToken := Value;
FGetTokenDateTime := now;
end;
function TIdSASLXOAuth.IsTokenExpired: Boolean;
var
FExpireDateTime: TDateTime;
begin
FExpireDateTime := IncSecond(FGetTokenDateTime, StrToInt(FExpireTime));
Result := FExpireDateTime <= Now
end;
end.
最後在FormCreate的事件上建立和添加一下XOAuth的SASL
procedure TForm1.FormCreate(Sender: TObject);
begin
xOAuthSASL := IdIMAP4.SASLMechanisms.Add;
xOAuthSASL.SASL := TIdSASLXOAuth.Create(Self);
end;
實作Authorization Code Flow做授權驗證
使用TsgcHTTP_OAuth2_Client前需要細部設定一下OAuth的屬性
- OAuth2Options:
- Client ID:用戶識別碼,直接帶入Azure AD的應用程式識別碼
- Client Secret:用戶識別密碼,由於我們這裡的Redirect URI在Azure AD上是一個Public Client因此這裡不需要填入Client Secret【點我前往參考詳細問題】
- Grant Type:授權方式,我們示範的是Authorization Code Flow因此直接選擇auth2Code💡 目前這個元件只支持Authorization Code Flow和Client Credentials Flow
- Password:無需設定
- Username:無需設定
- AuthorizationServerOptions:
- AuthURL:授權伺服器驗證URL
- Scope:請求的權限
- TokenURL:驗證成功後取得Token的URL
- LocalServerOptions:
- IP:IP位置
- Port:連結阜
- RedirectURL:這個RedirectURL需要添加到Azure AD的應用程式的平台認證否則會無法使用【點我前往參考設定】💡 這個元件啟動之後會執行一個WebServer是拿來讓你當作Redirect URI,這裡的相關設定都是關於這個WebServer的一些設定
屬性設定完成後接著來設定事件,Authorization Code Flow會有回傳AuthorizeCode和請求AccessToken的動作因此需要加入這兩個事件「OnAfterAccessToken」和「OnAfterAuthorizeCode」,另外如果想要知道請求錯誤訊息可以加入「OnErrorAuthorizeCode」和「OnErrorAccessToken」
- OnAfterAuthorizaCode:取得Auth Code的Callback
procedure TForm1.OAuth2_Authorization_CodeAfterAuthorizeCode(Sender: TObject;
const Code, State, Scope, RawParams: string; var Handled: Boolean);
begin
DoLog('Code: ' + Code + CRLF +
'State: ' + State + CRLF +
'Scope: ' + Scope);
end;
- OnAfterAccessToken:取得Access Token的Callback
procedure TForm1.OAuth2_Authorization_CodeAfterAccessToken(Sender: TObject;
const Access_Token, Token_Type, Expires_In, Refresh_Token, Scope,
RawParams: string; var Handled: Boolean);
begin
DoLog('AccessToken: ' + Access_Token + CRLF +
'Token_Type: ' + Token_Type + CRLF +
'Expires_In: ' + Expires_In + CRLF +
'Refresh_Token: ' + Refresh_Token + CRLF +
'Scope: ' + Scope);
TIdSASLXOAuth(xOAuthSASL.SASL).Token := Access_Token;
TIdSASLXOAuth(xOAuthSASL.SASL).ExpireTime := Expires_In;
TIdSASLXOAuth(xOAuthSASL.SASL).User := '你要授權登入的電子郵件名稱@outlook.com';
end;
總結
做到這裡是不是覺得哪裡怪怪的呢?給大家思考一下!
。
。
。
。
。
。
再思考一下XDD
。
。
。
。
沒錯問題就是出在Authorization Code Flow需要有「Resource Owner(這裡指的是人)」去做一個授權的動作,我們可以看到微軟給的Token ExpireTime是3600秒,等於一個小時要叫Resource Owner做一次登入和授權的動作(備註1),可是今天我們的IMAP是一個服務是不可能有Resource Owner可以來介入的,因此Authorization Code Flow是不適合我們的服務類型的,因此我們必須重新換一個OAuth的流程才行!
程式碼下載:【Delphi - OAuth2IMAP】
0 Comments
張貼留言