C#에서 OAuth 2.0 인증하기(JSON 방식) , 그리고 블로그 포스팅 하기 (Blogger API Post Insert) 흐름 안내입니다. Desktop Apps을 기준으로 하겠습니다.
첫 번째로 데스크탑 앱의 OAuth 2.0 흐름에 대해 알아보고, 두 번째로 OAuth 2.0에 대한 개념을 알아보도록 하겠습니다.
실행을 한 번 해보겠습니다. 예제코드는 아래에 있습니다.
코드검증자를 생성하여 OAuth2.0 서버에 승인 요청을 합니다.
승인 화면이 나타납니다.
권한부여 여부에 대해 사용자 확인하기
허용 여부 확인하기
애플리케이션으로 다시 돌아가라고 띄웁니다. (데스크탑 OAuth2 승인 과정을 루프백 아이피 어드레스로 처리했습니다.)
제목과 내용은 간단히 현재 시간 포스팅 하도록 했습니다.
code verifier 와 code_challenge 생성 과정 예제
//Generates state and PKCE values.
string state = GetRandomDataBase64url(32);
string code_verifier = GetRandomDataBase64url(32);
string code_challenge = GetBase64urlencodeNoPadding(GetSha256(code_verifier));
int randomUnusedPort = GetRandomUnusedPort();
//Creates a redirect URI using an available port on the loopback address.
string redirect_uri = string.Format("http://{0}:{1}/", IPAddress.Loopback, randomUnusedPort);
//Incoming(response)
string incoming_code = string.Empty;
string incoming_state = string.Empty;
#if DEBUG
Console.WriteLine("redirect_uri : " + redirect_uri);
#endif
//Creates an HttpListener to listen for requests on that redirect URI.
var httpListener = new HttpListener();
httpListener.Prefixes.Add(redirect_uri);
httpListener.Start();
#if DEBUG
Console.WriteLine("HttpListener Start");
#endif
//Creates the OAuth 2.0 authorization request.
string authorizationRequest = string.Format("{0}?response_type=code&scope={6}&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
c_authorizationEndpoint,
Uri.EscapeDataString(redirect_uri),
m_clientID,
state,
code_challenge,
c_code_challenge_method,
m_scope
);
//Opens request in the browser.
System.Diagnostics.Process.Start(authorizationRequest);
//Waits for the OAuth authorization response.
var context = await httpListener.GetContextAsync();
//Brings this app back to the foreground.
this.Activate();
//Sends an HTTP response to the browser.
var response = context.Response;
string responseString = "<html><head><meta http-equiv='refresh' content='10;url=https://google.com'></head><body>Please return to the app.</body></html>";
var buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
var responseOutput = response.OutputStream;
Task responseTask = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) =>
{
responseOutput.Close();
httpListener.Stop();
#if DEBUG
Console.WriteLine("HTTP server stopped. (HttpListener)");
#endif
});
//Checks for errors.
if (context.Request.QueryString.Get("error") != null)
{
#if DEBUG
Console.WriteLine(string.Format("OAuth authorization error: {0}.", context.Request.QueryString.Get("error")));
#endif
return;
}
if (context.Request.QueryString.Get("code") == null
|| context.Request.QueryString.Get("state") == null)
{
#if DEBUG
Console.WriteLine(string.Format("Malformed authorization response: {0}.", context.Request.QueryString));
#endif
return;
}
//Extracts the code
incoming_code = context.Request.QueryString.Get("code");
incoming_state = context.Request.QueryString.Get("state");
//Compares the receieved state to the expected value, to ensure that
//this app made the request which resulted in authorization.
if (incoming_state != state)
{
#if DEBUG
Console.WriteLine(string.Format("Received request with invalid state / resState : {0} , reqState : {1}", incoming_state, state));
#endif
return;
}
#if DEBUG
Console.WriteLine("Authorization code: " + incoming_code);
#endif
//Starts the code exchange at the Token Endpoint.
OAuth 2.0 참고 자료
아래의 링크를 참고하여 개발하세요. 😘
- [Google ID 플랫폼] https://developers.google.com/identity
- [Google ID 플랫폼 OAuth2 프로토콜] https://developers.google.com/identity/protocols/oauth2
- [Google Developers 플레이그라운드] https://developers.google.com/oauthplayground/
- [Google Obtaining OAuth 2.0 access tokens] https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier
- [Google Blogger API] https://developers.google.com/blogger
OAuth 2.0 소개
이제 OAuth 2.0에 대한 소개와 표준 역할, 흐름, API 액세스, 그리고 구글 클라우드 플랫폼을 통한 블로거 포스트를 더 자세히 알아보도록 하겠습니다. 목차는 다음과 같습니다.
- OAuth 2.0 소개
- OAuth 2.0 표준 역할
- OAuth 2.0 표준 흐름
- OAuth 2.0 구글 API 액세스(google identity)
- OAuth 2.0 구글 클라우드 플랫폼 블로거 포스트
- 구글 블로거 API(OAuth 예시)
OAuth 2.0 Authorization Framework(Request for Comments 6749)
OAuth는 "Open Authorization"의 약자로 회원가입 등의 비밀번호 정보 제공 없이 권한부여만으로 인증한 애플리케이션을 사용할 수 있게 하는 개방형 표준입니다.
상기는 롯데온 홈페이지 로그인 화면입니다. 카카오, 네이버, 페이스북, 또는 휴대폰으로 로그인을 할 수 있도록 하는 것이 OAuth 입니다.
OAuth 2.0 표준 역할
OAuth는 4가지 역할이 있습니다.
- 리소스 소유자(resource owner) => 리소스 접근을 허용할 수 있는 개체입니다. 보통 사용자 본인을 뜻합니다.
- 리소스 서버(resource server) => 액세스 토큰(access tokens)을 이용하여 접근할 수 있는 리소스의 서버입니다.
- 클라이언트(client) => 리소스와 인증 서버 간의 매개 역할을 합니다. 보통 애플리케이션입니다.
- 인증 서버(authorization server) => 소유자의 승낙 하에 클라이언트에게 액세스 토큰을 발행합니다.
OAuth 2.0 표준 흐름
- 클라이언트(애플리케이션)가 리소스 소유자에게 권한부여를 요청합니다.
- 리소스 소유자가 클라이언트에 대한 권한부여를 수락하면 다음단계를 진행합니다.
- 클라이언트가 부여된 권한으로 인증서버에 액세스 토큰을 요청합니다.
- 인증서버가 액세스 토큰 발행에 관한 검증작업을 진행하여 유효한 경우, 클라이언트에 액세스 토큰을 전달합니다.
- 클라이언트는 부여받은 액세스 토큰으로 리소스 서버에 리소스 접근을 요청합니다.
- 리소스 서버는 검증 이후 클라이언트의 리소스 사용을 허가합니다.
OAuth 2.0 구글 API 액세스(google identity)
구글 API 또한 OAuth 표준을 지원합니다. 그 중에서 오늘 알아볼 것은 자바스크립트 애플리케이션입니다.
자바스크립트 애플리케이션은 다음과 같은 과정으로 진행합니다.
- 토큰의 요청과 응답
- 토큰의 유효성 검증
- 토큰을 통한 API 호출
OAuth 2.0 구글 클라우드 플랫폼 블로거 포스트
OAuth 2.0과 구글 클라우드 플랫폼을 사용하여 블로거(구글 블로그)에 포스트를 하는 예제를 다루어보겠습니다.
API를 통한 블로거 포스트
OAuth를 이용하여 블로거 포스팅을 해봅시다. 액세스 토큰을 발행하기 위해서는 액세스키와 시크릿키가 필요합니다. 구글 클라우드에서 액세스키는 클라이언트 아이디로, 스크릿키는 클라이언트 시크릿으로 표현하고 있습니다.
OAuth 2.0 클라이언트 ID
클라우드 콘솔에서 사용자 인증 정보, OAuth 2.0 클라이언트 ID를 등록하였습니다.
구글 블로거 API(OAuth 예시)
액세스 토큰을 발행하기 위해 클라우드 콘솔에 등록한 클라이언트 아이디와 시크릿을 입력합니다. 블로거 아이디는 구글 블로그 환경 설정에 가면 확인할 수 있습니다.
OAuth 2.0 예시
OAuth 2.0 인증을 진행해보겠습니다. 자바스크립트 애플리케이션에서 로그인 화면으로 이동합니다.
액세스 허용여부를 선택합니다.
액세스 요청 이후 리디렉션 사이트로 이동하며, 구글 OAuth 승인 서버로 구글 블로거 API 권한부여 요청 및 액세스 토큰 유효성을 검증합니다. 그리고 API 응답을 콘솔로 전달합니다.
이번 예시는 권한이 부여되면 바로 블로거 포스팅을 하도록 진행하였습니다.
인증 이후 블로거 글이 등록된 모습입니다. (제목은 Title, 내용은 Content로 발행하였습니다)
액세스 토큰 발행 즉시 Title과 Content로 블로거 글을 작성하도록 해보았습니다.
권한 요청 및 요청 응답 상태 확인 예시
public static async void DoSendAuthRequest(Window _window)
{
//Generate State and PKCE(Proof Key for Code Exchange) values
string state = CoreBeomSang.GetRandomDataBase64url(32);
string codeVerifier = CoreBeomSang.GetRandomDataBase64url(32);
string codeChallenge = CoreBeomSang.GetBase64urlencodeNoPadding(CoreBeomSang.GetSha256(codeVerifier));
int randomUnusedPort = CoreBeomSang.GetRandomUnusedPort();
//Create a redirect URI
string redirectUri = string.Format("http://{0}:{1}/", IPAddress.Loopback, randomUnusedPort);
//Response Values
string resCode = string.Empty;
string resState = string.Empty;
//UI
g_lblOAuthToken.Content = "OAuth 인증을 시도합니다.";
//Create an HttpListener to listen for requests on the redirect URI
var httpListener = new HttpListener();
httpListener.Prefixes.Add(redirectUri);
httpListener.Start();
//Create the OAuth 2.0 authorization request
string authorizationRequest = string.Format("{0}?redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}&response_type=code&scope={6}",
c_authorizationEndpoint,
Uri.EscapeDataString(redirectUri),
ACCESSKEY,
state,
codeChallenge,
c_code_challenge_method,
c_bloggerEndpoint
);
//Open request in the browser
Process.Start(authorizationRequest);
//Wait for the OAuth authorization response
HttpListenerContext context = await httpListener.GetContextAsync();
//Bring this app back to the foreground
_window.Activate();
//Send an HTTP response to the browser
var response = context.Response;
string responseString = $"<html><head><meta http-equiv='refresh' content='10;url={ViewBeomSang.g_beomsangTistory}' charset='UTF-8'></head><body><p>인증절차를 종료하였습니다.</p><p>애플리케이션으로 돌아가 주세요!</p><p>감사합니다.</p><p>범상 드림.</p></body></html>";
var buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
var responseOutput = response.OutputStream;
Task responseTask = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) =>
{
responseOutput.Close();
httpListener.Stop();
Debug.WriteLine("HTTP server stopped. (HttpListener)");
});
//Check for errors
if (context.Request.QueryString.Get("error") != null)
{
g_lblOAuthToken.Content = $"OAuth authorization error: {context.Request.QueryString.Get("error")}";
return;
}
//Check for malformed response
if (context.Request.QueryString.Get("code") == null
|| context.Request.QueryString.Get("state") == null)
{
g_lblOAuthToken.Content = string.Format($"Malformed authorization response: {context.Request.QueryString}");
return;
}
//Extract the code(authorization code)
resCode = context.Request.QueryString.Get("code");
resState = context.Request.QueryString.Get("state");
//Compare the receieved state to the expected value, to ensure that this app made the request which resulted in authorization
if (resState != state)
{
g_lblOAuthToken.Content = $"Received request with invalid state / resState : {resState} , reqState : {state}";
return;
}
//Start the code exchange at the Token Endpoint
DoExchangeAuthCode(resCode, codeVerifier, redirectUri);
}
액세스 토큰 발급 예시
private static async void DoExchangeAuthCode(string _code, string _codeVerifier, string _redirectURI)
{
//UI
g_lblOAuthToken.Content = "Exchanging code for tokens Starts";
//Exchanging code for tokens Starts
//Build the request
string tokenRequestURI = c_tokenEndpoint;
string tokenRequestBody = string.Format("code={0}&redirect_uri={1}&client_id={2}&code_verifier={3}&client_secret={4}&scope=&grant_type=authorization_code",
_code,
Uri.EscapeDataString(_redirectURI),
ACCESSKEY,
_codeVerifier,
SECRETKEY
);
try
{
byte[] btReqBody = Encoding.ASCII.GetBytes(tokenRequestBody);
HttpWebRequest tokenRequest = (HttpWebRequest)WebRequest.Create(tokenRequestURI);
tokenRequest.Method = "POST";
tokenRequest.ContentType = "application/x-www-form-urlencoded";
tokenRequest.Accept = "Accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
tokenRequest.ContentLength = btReqBody.Length;
using (Stream stream = tokenRequest.GetRequestStream())
{
await stream.WriteAsync(btReqBody, 0, btReqBody.Length);
stream.Close();
}
//Get the response
using (WebResponse tokenResponse = await tokenRequest.GetResponseAsync())
{
using (StreamReader reader = new StreamReader(tokenResponse.GetResponseStream()))
{
//Read response body
string resTxt = await reader.ReadToEndAsync();
//Convert to dictionary
Dictionary<string, string> tokenEndpointDecoded = JsonConvert.DeserializeObject<Dictionary<string, string>>(resTxt);
g_access_token = tokenEndpointDecoded["access_token"];
g_refresh_token = tokenEndpointDecoded["refresh_token"];
g_expires_in = tokenEndpointDecoded["expires_in"];
g_lblOAuthToken.Content = $"접근토큰 : {g_access_token.Substring(0, 5) }***** / 만료시간(초) : {g_expires_in}";
//Test Insert
DoInsertBloggerPost();
}
}
}
catch (Exception ex)
{
CoreBeomSang.CatchOAuthWebEx(ex);
g_lblOAuthToken.Content = "접근토큰을 획득하지 못했습니다. 예외 : " + ex;
}
}