OAuth(개방형 인증)는 개방형 표준으로, 사용자가 제3자 애플리케이션이 특정 웹사이트에 저장된 개인 리소스(사진, 비디오, 연락처 목록 등)에 접근할 수 있도록 허용하면서, 사용자 이름과 비밀번호를 제3자 애플리케이션에 제공할 필요가 없게 해줍니다.
OAuth는 사용자가 사용자 이름과 비밀번호 대신 특정 토큰을 제공하여 특정 서비스 제공자에게 저장된 데이터에 접근할 수 있게 합니다. 각 토큰은 특정 웹사이트(예: 비디오 편집 웹사이트)가 특정 기간(예: 다음 2시간 내)에 특정 리소스(예: 특정 앨범의 비디오)에 접근하도록 권한을 부여합니다. 이렇게 OAuth는 사용자가 제3자 웹사이트가 다른 서비스 제공자에 저장된 특정 정보에 접근하도록 허용하게 하여 모든 콘텐츠에 접근하는 것이 아닌 일부 특정 정보만 접근할 수 있게 합니다.
OAuth 2.0의 실행 흐름(RFC 6749)을 설명하기 위해 다음과 같은 시나리오를 상상해 봅시다: 사용자가 루오 웹(Luowang)에서 읽고 있지만, 즐겨찾기 기능을 사용하려면 로그인이 필요합니다. 그런 다음 빠른 로그인 방식으로 위보(Weibo) 계정과 비밀번호를 사용하여 로그인하면, 루오 웹은 위보의 계정 정보 등에 접근할 수 있게 되고, 루오 웹에도 로그인된 상태가 됩니다. 마지막으로 사용자는 즐겨찾기 기능을 사용할 수 있습니다.
위 시나리오를 바탕으로 OAuth 2.0의 실행 흐름을 자세히 설명합니다:
- (A) 사용자가 루오 웹에 로그인하고, 루오 웹은 사용자의 로그인 인증을 요청합니다(실제 작업은 사용자가 루오 웹에서 로그인하는 것입니다).
- (B) 사용자가 로그인 인증에 동의합니다(실제 작업은 사용자가 빠른 로그인을 열고 위보의 계정과 비밀번호를 입력하는 것입니다).
- (C) 루오 웹이 위보의 인증 페이지로 이동하고 인증을 요청합니다(여기서 위보 계정과 비밀번호가 필요합니다).
- (D) 위보가 사용자가 입력한 계정과 비밀번호를 확인하고, 성공하면 access_token을 루오 웹에 반환합니다.
- (E) 루오 웹이 반환된 access_token을 사용하여 위보에 요청합니다.
- (F) 위보가 루오 웹이 제공한 access_token을 확인하고, 성공하면 위보의 계정 정보를 루오 웹에 반환합니다.
이름 설명:
- Client -> 루오 웹
- Resource Owner -> 사용자
- Authorization Server -> 위보 인증 서비스
- Resource Server -> 위보 리소스 서비스
OAuth 2.0의 네 가지 인증 모드:
- 인증 코드 모드(authorization code)
- 단순화 모드(implicit)
- 리소스 소유자 자격 증명 모드(resource owner password credentials)
- 클라이언트 모드(client credentials)
이제 ASP.NET WebApi OWIN을 사용하여 위 네 가지 인증 모드를 각각 구현해 보겠습니다.
- 인증 코드 모드(authorization code)
간단한 설명: 루오 웹이 일부 인증 정보를 제공하여 위보 인증 서비스로부터 authorization_code를 얻은 다음, 이 authorization_code를 사용하여 access_token을 얻습니다. 루오 웹은 위보 인증 서비스에 두 번 요청해야 합니다.
첫 번째 요청(인증 서비스에 authorization_code를 얻기)에 필요한 매개변수:
- grant_type: 필수, 인증 모드, 값은 "authorization_code"
- response_type: 필수, 인증 유형, 값은 고정적으로 "code"
- client_id: 필수, 클라이언트 ID
- redirect_uri: 필수, 리디렉션 URI, URL에 authorization_code가 포함됩니다.
- scope: 선택, 요청된 권한 범위, 예를 들어 위보 인증 서비스의 값은 follow_app_official_microblog
- state: 선택, 클라이언트의 현재 상태, 임의 값을 지정할 수 있으며, 인증 서버가 이 값을 그대로 반환합니다. 예를 들어 위보 인증 서비스의 값은 weibo
두 번째 요청(인증 서비스에 access_token을 얻기)에 필요한 매개변수:
- grant_type: 필수, 인증 모드, 값은 "authorization_code"
- code: 필수, 인증 코드, 값은 위 요청에서 반환된 authorization_code
- redirect_uri: 필수, 리디렉션 URI, 위 요청의 redirect_uri 값과 동일해야 합니다.
- client_id: 필수, 클라이언트 ID
두 번째 요청(인증 서비스에 access_token을 얻기)에서 반환되는 매개변수:
- access_token: 액세스 토큰
- token_type: 토큰 유형, 일반적으로 "bearer" 값
- expires_in: 만료 시간(초 단위)
- refresh_token: 새로 고침 토큰, 다음 액세스 토큰을 얻는 데 사용됩니다.
- scope: 권한 범위
ASP.NET WebApi OWIN에 필요한 패키지 설치:
- Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.OAuth
- Microsoft.Owin.Security.Cookies
- Microsoft.AspNet.Identity.Owin
프로젝트에 Startup.cs 파일을 생성하고 다음 코드를 추가합니다:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
var OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
AuthenticationMode = AuthenticationMode.Active,
TokenEndpointPath = new PathString("/token"), // access_token을 얻기 위한 인증 서비스 요청 주소
AuthorizeEndpointPath = new PathString("/authorize"), // authorization_code를 얻기 위한 인증 서비스 요청 주소
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), // access_token 만료 시간
Provider = new CustomAuthorizationServerProvider(), // access_token 관련 인증 서비스
AuthorizationCodeProvider = new CustomAuthorizationCodeProvider(), // authorization_code 인증 서비스
RefreshTokenProvider = new CustomRefreshTokenProvider() // refresh_token 인증 서비스
};
app.UseOAuthBearerTokens(OAuthOptions); // token_type이 bearer 방식임을 나타냅니다
}
}
CustomAuthorizationServerProvider 예제 코드:
public class CustomAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
/// <summary>
/// client 정보 검증
/// </summary>
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId;
string clientSecret;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (clientId != "demo_client")
{
context.SetError("invalid_client", "client is not valid");
return;
}
context.Validated();
}
/// <summary>
/// authorization_code 생성(authorization code 인증 방식), access_token 생성(implicit 인증 모드)
/// </summary>
public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
if (context.AuthorizeRequest.IsImplicitGrantType)
{
// implicit 인증 방식
var identity = new ClaimsIdentity("Bearer");
context.OwinContext.Authentication.SignIn(identity);
context.RequestCompleted();
}
else if (context.AuthorizeRequest.IsAuthorizationCodeGrantType)
{
// authorization code 인증 방식
var redirectUri = context.Request.Query["redirect_uri"];
var clientId = context.Request.Query["client_id"];
var identity = new ClaimsIdentity(new GenericIdentity(
clientId, OAuthDefaults.AuthenticationType));
var authorizeCodeContext = new AuthenticationTokenCreateContext(
context.OwinContext,
context.Options.AuthorizationCodeFormat,
new AuthenticationTicket(
identity,
new AuthenticationProperties(new Dictionary<string, string>
{
{"client_id", clientId},
{"redirect_uri", redirectUri}
})
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.Add(context.Options.AuthorizationCodeExpireTimeSpan)
}));
await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext);
context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));
context.RequestCompleted();
}
}
/// <summary>
/// authorization_code 요청 검증
/// </summary>
public override async Task ValidateAuthorizeRequest(OAuthValidateAuthorizeRequestContext context)
{
if (context.AuthorizeRequest.ClientId == "demo_client" &&
(context.AuthorizeRequest.IsAuthorizationCodeGrantType || context.AuthorizeRequest.IsImplicitGrantType))
{
context.Validated();
}
else
{
context.Rejected();
}
}
/// <summary>
/// redirect_uri 검증
/// </summary>
public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
context.Validated(context.RedirectUri);
}
/// <summary>
/// access_token 요청 검증
/// </summary>
public override async Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
{
if (context.TokenRequest.IsAuthorizationCodeGrantType || context.TokenRequest.IsRefreshTokenGrantType)
{
context.Validated();
}
else
{
context.Rejected();
}
}
}
CustomAuthorizationCodeProvider 예제 코드:
public class CustomAuthorizationCodeProvider : AuthenticationTokenProvider
{
private readonly ConcurrentDictionary<string, string> _authCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
/// <summary>
/// authorization_code 생성
/// </summary>
public override void Create(AuthenticationTokenCreateContext context)
{
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authCodes[context.Token] = context.SerializeTicket();
}
/// <summary>
/// authorization_code를 access_token으로 변환
/// </summary>
public override void Receive(AuthenticationTokenReceiveContext context)
{
string value;
if (_authCodes.TryRemove(context.Token, out value))
{
context.DeserializeTicket(value);
}
}
}
CustomRefreshTokenProvider 예제 코드:
public class CustomRefreshTokenProvider : AuthenticationTokenProvider
{
private static ConcurrentDictionary<string, string> _refreshTokens = new ConcurrentDictionary<string, string>();
/// <summary>
/// refresh_token 생성
/// </summary>
public override void Create(AuthenticationTokenCreateContext context)
{
context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(60);
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_refreshTokens[context.Token] = context.SerializeTicket();
}
/// <summary>
/// refresh_token을 access_token으로 변환
/// </summary>
public override void Receive(AuthenticationTokenReceiveContext context)
{
string value;
if (_refreshTokens.TryRemove(context.Token, out value))
{
context.DeserializeTicket(value);
}
}
}
authorization_code를 수락하기 위한 API가 필요합니다(redirect_uri의 콜백 점프). 구현 코드는 다음과 같습니다:
public class AuthorizationCodeController : ApiController
{
[HttpGet]
[Route("api/auth-code")]
public HttpResponseMessage Get(string code)
{
return new HttpResponseMessage()
{
Content = new StringContent(code, Encoding.UTF8, "text/plain")
};
}
}
기본적으로 위 코드는 구현되었습니다. 단위 테스트 코드는 다음과 같습니다:
public class OAuthClientTest
{
private const string HOST_ADDRESS = "http://localhost:8001";
private IDisposable _webApp;
private static HttpClient _httpClient;
public OAuthClientTest()
{
_webApp = WebApp.Start<Startup>(HOST_ADDRESS);
Console.WriteLine("Web API 시작됨!");
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri(HOST_ADDRESS);
Console.WriteLine("HttpClient 시작됨!");
}
private static async Task<TokenResponse> GetAccessToken(string grantType, string refreshToken = null, string userName = null, string password = null, string authCode = null)
{
var clientId = "demo_client";
var clientSecret = "demo_secret";
var parameters = new Dictionary<string, string>();
parameters.Add("grant_type", grantType);
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
{
parameters.Add("username", userName);
parameters.Add("password", password);
}
if (!string.IsNullOrEmpty(authCode))
{
parameters.Add("code", authCode);
parameters.Add("redirect_uri", "http://localhost:8001/api/auth-code"); // authorization_code를 얻을 때의 redirect_uri와 일치해야 합니다
}
if (!string.IsNullOrEmpty(refreshToken))
{
parameters.Add("refresh_token", refreshToken);
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret)));
var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
var responseValue = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(response.StatusCode);
Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
return null;
}
return await response.Content.ReadAsAsync<TokenResponse>();
}
private static async Task<string> GetAuthCode()
{
var clientId = "demo_client";
var response = await _httpClient.GetAsync($"/authorize?grant_type=authorization_code&response_type=code&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode("http://localhost:8001/api/auth_code")}");
var authCode = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(response.StatusCode);
Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
return null;
}
return authCode;
}
[Fact]
public async Task OAuth_AuthorizationCode_Test()
{
var authCode = GetAuthCode().Result; // authorization_code 얻기
var tokenResponse = GetAccessToken("authorization_code", null, null, null, authCode).Result; // authorization_code로 access_token 얻기
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
var response = await _httpClient.GetAsync($"/api/values");
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(response.StatusCode);
Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
}
Console.WriteLine(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Thread.Sleep(10000);
var tokenResponseTwo = GetAccessToken("refresh_token", tokenResponse.RefreshToken).Result; // refresh_token으로 access_token 얻기
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
var responseTwo = await _httpClient.GetAsync($"/api/values");
Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
}
}
Startup에서 구성된 access_token의 만료 시간은 10초이며, 스레드를 10초 동안 대기시키는 것은 refresh_token을 테스트하기 위함입니다.
위 단위 테스트 코드는 성공적으로 실행될 수 있습니다. 물론 Postman을 사용하여 요청을 시뮬레이션하여 테스트할 수도 있습니다.
- 단순화 모드(implicit grant type)
간단한 설명: 인증 코드 모드의 단순화 버전으로, authorization_code를 생략하고 access_token이 URL 매개변수로 반환됩니다(예: #token=xxxx).
인증 서비스에 대한 요청(한 번만)에 필요한 매개변수:
- response_type: 필수, 인증 유형, 값은 고정적으로 "token"
- client_id: 필수, 클라이언트 ID
- redirect_uri: 필수, 리디렉션 URI, URL에 access_token이 포함됩니다.
- scope: 선택, 요청된 권한 범위, 예를 들어 위보 인증 서비스의 값은 follow_app_official_microblog
- state: 선택, 클라이언트의 현재 상태, 임의 값을 지정할 수 있으며, 인증 서버가 이 값을 그대로 반환합니다. 예를 들어 위보 인증 서비스의 값은 weibo
주의할 점은, 단순화 모드 요청 매개변수에 grant_type이 필요하지 않으며, HTTP GET을 사용하여 직접 요청할 수 있다는 것입니다.
Startup 코드:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
var OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
AuthenticationMode = AuthenticationMode.Active,
TokenEndpointPath = new PathString("/token"), // access_token을 얻기 위한 인증 서비스 요청 주소
AuthorizeEndpointPath = new PathString("/authorize"), // authorization_code를 얻기 위한 인증 서비스 요청 주소
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), // access_token 만료 시간
Provider = new CustomAuthorizationServerProvider(), // access_token 관련 인증 서비스
RefreshTokenProvider = new CustomRefreshTokenProvider() // refresh_token 인증 서비스
};
app.UseOAuthBearerTokens(OAuthOptions); // token_type이 bearer 방식임을 나타냅니다
}
}
CustomRefreshTokenProvider, CustomAuthorizationServerProvider의 코드는 위 인증 코드 모드와 동일하며, CustomAuthorizationServerProvider의 AuthorizeEndpoint 메서드에 IsImplicitGrantType 판단이 있습니다. 예제 코드:
var identity = new ClaimsIdentity("Bearer");
context.OwinContext.Authentication.SignIn(identity);
context.RequestCompleted();
이 코드는 redirect_uri로 콜백하고 access_token을 함께 전달하며, 수신 예제 코드:
[HttpGet]
[Route("api/access-token")]
public HttpResponseMessage GetAccessToken()
{
var url = Request.RequestUri;
return new HttpResponseMessage()
{
Content = new StringContent("", Encoding.UTF8, "text/plain")
};
}
단위 테스트 코드:
[Fact]
public async Task OAuth_Implicit_Test()
{
var clientId = "demo_client";
var tokenResponse = await _httpClient.GetAsync($"/authorize?response_type=token&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode("http://localhost:8001/api/access_token")}");
// redirect_uri: http://localhost:8001/api/access_token#access_token=AQAAANCMnd8BFdERjHoAwE_Cl-sBAAAAfoPB4HZ0PUe-X6h0UUs2q42&token_type=bearer&expires_in=10
var accessToken = "";// redirect_uri에서 가져오기
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _httpClient.GetAsync($"/api/values");
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(response.StatusCode);
Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
}
Console.WriteLine(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
redirect_uri의 access_token 매개변수 값은 URL의 # 뒤에 있어서 백엔드에서 가져오기 어렵기 때문에, 여기서의 단위 테스트는 예시일 뿐 실행되지 않으며, Postman을 사용하여 테스트하는 것을 권장합니다.
- 리소스 소유자 자격 증명 모드(resource owner password credentials)
간단한 설명: 처음에 설명한 OAuth 인증 흐름에서 사실상 비밀번호 모드입니다. 루오 웹이 인증 요청을 시작하고, 사용자가 위보 인증 페이지에 계정과 비밀번호를 입력하면, 확인이 성공하면 access_token이 반환됩니다. 따라서 이 과정에서 사용자가 입력한 계정과 비밀번호는 루오 웹과 아무 관련이 없으며, 계정 정보가 제3자에 의해 도용될 위험이 없습니다.
인증 서비스에 대한 요청(한 번만)에 필요한 매개변수:
- grant_type: 필수, 인증 모드, 값은 고정적으로 "password"
- username: 필수, 사용자 이름
- password: 필수, 사용자 비밀번호
- scope: 선택, 요청된 권한 범위, 예를 들어 위보 인증 서비스의 값은 follow_app_official_microblog
Startup 코드:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
var OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
AuthenticationMode = AuthenticationMode.Active,
TokenEndpointPath = new PathString("/token"), // access_token을 얻기 위한 인증 서비스 요청 주소
AuthorizeEndpointPath = new PathString("/authorize"), // authorization_code를 얻기 위한 인증 서비스 요청 주소
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), // access_token 만료 시간
Provider = new CustomAuthorizationServerProvider(), // access_token 관련 인증 서비스
RefreshTokenProvider = new CustomRefreshTokenProvider() // refresh_token 인증 서비스
};
app.UseOAuthBearerTokens(OAuthOptions); // token_type이 bearer 방식임을 나타냅니다
}
}
CustomAuthorizationServerProvider 예제 코드:
public class CustomAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
/// <summary>
/// client 정보 검증
/// </summary>
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId;
string clientSecret;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (clientId != "demo_client")
{
context.SetError("invalid_client", "client is not valid");
return;
}
context.Validated();
}
/// <summary>
/// access_token 생성(resource owner password credentials 인증 방식)
/// </summary>
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
if (string.IsNullOrEmpty(context.UserName))
{
context.SetError("invalid_username", "username is not valid");
return;
}
if (string.IsNullOrEmpty(context.Password))
{
context.SetError("invalid_password", "password is not valid");
return;
}
if (context.UserName != "demo_user" || context.Password != "demo_pass")
{
context.SetError("invalid_identity", "username or password is not valid");
return;
}
var OAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
OAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
context.Validated(OAuthIdentity);
}
}
GrantResourceOwnerCredentials 내부에서 외부 서비스를 호출하여 사용자 계정 정보를 검증할 수 있습니다.
단위 테스트 코드:
[Fact]
public async Task OAuth_Password_Test()
{
var tokenResponse = GetAccessToken("password", null, "demo_user", "demo_pass").Result; // access_token 얻기
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
var response = await _httpClient.GetAsync($"/api/values");
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(response.StatusCode);
Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
}
Console.WriteLine(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Thread.Sleep(10000);
var tokenResponseTwo = GetAccessToken("refresh_token", tokenResponse.RefreshToken).Result;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
var responseTwo = await _httpClient.GetAsync($"/api/values");
Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
}
- 클라이언트 모드(Client Credentials Grant)
간단한 설명: 이름 그대로 클라이언트 모드는 클라이언트가 인증 서비스에 직접 요청을 보내는 것으로, 사용자와는 관련이 없습니다. 즉, 루오 웹이 위보에 직접 인증 요청을 제출하며, 이러한 요청에는 사용자 정보가 포함되지 않습니다. 일반적으로 애플리케이션 간의 직접 상호작용 등에 사용됩니다.
인증 서비스에 대한 요청(한 번만)에 필요한 매개변수:
- grant_type: 필수, 인증 모드, 값은 고정적으로 "client_credentials"
- client_id: 필수, 클라이언트 ID
- client_secret: 필수, 클라이언트 비밀번호
- scope: 선택, 요청된 권한 범위, 예를 들어 위보 인증 서비스의 값은 follow_app_official_microblog
Startup 코드:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
var OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
AuthenticationMode = AuthenticationMode.Active,
TokenEndpointPath = new PathString("/token"), // access_token을 얻기 위한 인증 서비스 요청 주소
AuthorizeEndpointPath = new PathString("/authorize"), // authorization_code를 얻기 위한 인증 서비스 요청 주소
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), // access_token 만료 시간
Provider = new CustomAuthorizationServerProvider(), // access_token 관련 인증 서비스
RefreshTokenProvider = new CustomRefreshTokenProvider() // refresh_token 인증 서비스
};
app.UseOAuthBearerTokens(OAuthOptions); // token_type이 bearer 방식임을 나타냅니다
}
}
CustomAuthorizationServerProvider 예제 코드:
public class CustomAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
/// <summary>
/// client 정보 검증
/// </summary>
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId;
string clientSecret;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (clientId != "demo_client" || clientSecret != "demo_secret")
{
context.SetError("invalid_client", "client or clientSecret is not valid");
return;
}
context.Validated();
}
/// <summary>
/// access_token 생성(client credentials 인증 방식)
/// </summary>
public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(
context.ClientId, OAuthDefaults.AuthenticationType),
context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
}
}
다른 인증 모드와 달리, 클라이언트 인증 모드에서는 client_secret을 검증해야 합니다(ValidateClientAuthentication).
단위 테스트 코드:
[Fact]
public async Task OAuth_ClientCredentials_Test()
{
var tokenResponse = GetAccessToken("client_credentials").Result; // access_token 얻기
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
var response = await _httpClient.GetAsync($"/api/values");
if (response.StatusCode != HttpStatusCode.OK)
{
Console.WriteLine(response.StatusCode);
Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
}
Console.WriteLine(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Thread.Sleep(10000);
var tokenResponseTwo = GetAccessToken("refresh_token", tokenResponse.RefreshToken).Result;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
var responseTwo = await _httpClient.GetAsync($"/api/values");
Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
}
위 네 가지 인증 모드 외에도 새로 고침 토�인(refresh token)이 있습니다. 단위 테스트 코드에서 이미 구현되었으며, 다음 두 가지 추가 매개변수가 필요합니다:
- grant_type: 필수, 인증 모드, 값은 고정적으로 "refresh_token"
- refresh_token: 필수, 인증에서 반환된 refresh_token
마지막으로 네 가지 인증 모드의 적용 시나리오를 요약합니다:
- 인증 코드 모드(authorization code): authorization_code를 도입하여 시스템 보안성을 높일 수 있으며, 클라이언트 애플리케이션 시나리오와 유사하지만 일반적으로 서버 측에서 사용됩니다.
- 단순화 모드(implicit): 서버 측의 개입이 필요 없으며, 프론트엔드에서 직접 완료할 수 있으며, 일반적으로 프론트엔드 작업에 사용됩니다.
- 리소스 소유자 자격 증명 모드(resource owner password credentials): 사용자 계정과 관련이 있으며, 일반적으로 제3자 로그인에 사용됩니다.
- 클라이언트 모드(client credentials): 사용자와 무관하며, 일반적으로 애플리케이션과 API 간의 상호작용 시나리오에 사용됩니다. 예를 들어 루오 웹이 API를 개방하여 제3자 개발자가 데이터를 호출할 수 있도록 하는 등입니다.