SAML을 이용한 SSO Service의 구현
1. 개요
- Service Provider : 서비스를 제공하는 주체
- 유저 : 서비스를 이용하는 사용자
- Identify Provider : 유저에 대한 인증을 담당하는 주체
Platform에 관계없이 다양한 환경에서 표준적인 방법으로 SSO 구현이 가능하다는 것이다.
실제 다양한 iPhone등 다양한 플랫폼에서 테스트해본 결과 잘 동작한다.
2. SAML Basic Steps
Google에서도 아래 그림을 제시하고 있다.
- 1단계 : 유저는 서비스 제공자(Service Provider)에게 접근한다. (서비스 이용을 위하여)
- 2단계 : 서비스 제공자는 SAMLRequest를 생성한다. 생성된 SAMLRequest은 XML format의 텍스트로 구성된다.
- 3단계 : 유저의 브라우저를 이용하여 인증 제공자(Identity Provider)로 Redirect 한다.
- 4단계 : 인증 제공자(Identity Provider)는 SAMLRequest를 파싱하고 유저인증을 진행한다.
- 5단계 : 인증제공자(Identity Provider)는 SAML Response를 생성한다.
- 6단계 : 인증제공자(Identity Provider)는 유저 브라우저를 이용하여 SAMLResponse data를 ACS로 전달한다.
- 7단계 : ACS는 Service Provider가 운영하게 되는데 SAMLResponse를 확인하고 실제 서비스 사이트로 유저를 Forwarding한다.
- 8단계 : 유저는 로그인에 성공하고 서비스를 제공받는다.
3. SAML SSO 실제 구현
3. SAML SSO 실제 구현
3.1 사전 준비
3.1 사전 준비
필요한 핵심 Library는 아래와 같다.
OpenSAML 2.0 Library
필요한 핵심 Library는 아래와 같다.
필요한 핵심 Library는 아래와 같다.
OpenSAML 2.0 Library
-
- download from http://www.opensaml.org
- OpenSAML 2.0은 JDK1.5에서 동작
- xmldsig, xmlsec library,
- from sun java web services development pack,
- http://java.sun.com/webservices/downloads/previous/webservicespack.jsp
~JDOM library
- from sun java web services development pack,
RSA Keypair를 준비한다.
openssl을 이용한 keypair 생성 commandRSA Keypair를 준비한다.
$ openssl genrsa -out sso_private.pem 1024 $ openssl rsa -in sso_private.pem -pubout -ourform DER -out sso_public.der $ openssl pkcs8 -topk8 -inform PEM -outform DER -in sso_private.pem -out sso_private.der -nocrypt |
생성된 2개의 파일은 sso_private.der, sso_public.der이다.
한가지 주의할점은 sso_public.der은 우리가 통상적으로 알고 있는 인증서(certificate)가 아니다.
certifificate는 인증기관으로부터 public key의 유효성을 인정받은 경우에 인증기관의 전자서명이 첨부되어 발행되는데
여기서는 original rsa public key만으로 SSO를 구현한다.
대신 신뢰성을 확보하기 위하여 identity provider의 public key는 신뢰성 있는 방식으로 service provider에게 전달되어야하며
이 전달과정이나 identity provider의 private key 관리에는 주의를 기울여야한다.
SAML에서 이용하는 xmldsig spec이나 구현 사례를 봐도 인증서를 탑재하여 SAML SSO를 분명히 진행할 수 있다.
이 경우에는 identity provider의 public key를 특별한 방법으로 전달할 필요 없이
service provider는 신뢰할 수 있는 CA(Certificate Authority)가 보장하는 전자인증서를 확보하고 인증서 CRL 또는 OCSP checking등을 추가할 수 있을것이다.
3.2 SAMLRequest 생성 및 redirect
3.2 SAMLRequest 생성 및 redirect
3.2.1 ServiceProviderForm : 실제 유저가 최초로 접근하는 페이지이다. 여기서부터 시작
3.2.1 ServiceProviderForm : 실제 유저가 최초로 접근하는 페이지이다. 여기서부터 시작
<form name="ServiceProviderForm" action="https://api.paygate.net/t/sso/saml/CreateRequestServlet.jsp" method="post"> <input type="hidden" name="loginForm" value="https://api.paygate.net/t/sso/saml/login_form.jsp" /> <input type="hideen" name="providerName" value="paygate.net" /> <input type="hidden" name="RelayState" value="https://api.paygate.net/t/sso/saml/result_view.jsp" /> <input type="hidden" name="acsURI" value="https://api.paygate.net/t/sso/saml/ACS.jsp" /> <input type="submit" value="Sign On"> </form> |
3.2.2 CreateRequestServlet : SAMLRequst 데이터 생성 단계
3.2.2 CreateRequestServlet : SAMLRequst 데이터 생성 단계
- 먼저 Parameter를 받고
String providerName = request.getParameter("providerName");
String RelayState = request.getParameter("RelayState");
String acsURI = request.getParameter("acsURI");
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String ssoURL = request.getParameter("loginForm"); String providerName = request.getParameter("providerName"); String RelayState = request.getParameter("RelayState"); String acsURI = request.getParameter("acsURI"); ..... } |
- SAMLRequest xml data를 생성
private String createAuthnRequest(String acsURL, String providerName) throws SamlException { String filepath = getServletContext().getRealPath("t/sso/saml/templates/" + SAML_REQUEST_TEMPLATE); String authnRequest = Util.readFileContents(filepath); authnRequest = StringUtils.replace(authnRequest, "##PROVIDER_NAME##", providerName); authnRequest = StringUtils.replace(authnRequest, "##ACS_URL##", acsURL); authnRequest = StringUtils.replace(authnRequest, "##AUTHN_ID##", Util.createID()); authnRequest = StringUtils.replace(authnRequest, "##ISSUE_INSTANT##", Util.getDateAndTime()); return authnRequest; } |
- Identity Provider로 redirect할 URL생성
private String computeURL(String ssoURL, String authnRequest, String RelayState) throws SamlException { StringBuffer buf = new StringBuffer(); try { buf.append(ssoURL); buf.append("?SAMLRequest="); buf.append(RequestUtil.encodeMessage(authnRequest)); buf.append("&RelayState="); buf.append(URLEncoder.encode(RelayState)); return buf.toString(); } catch (UnsupportedEncodingException e) { throw new SamlException( "Error encoding SAML Request into URL: Check encoding scheme - " + e.getMessage()); } catch (IOException e) { throw new SamlException( "Error encoding SAML Request into URL: Check encoding scheme - " + e.getMessage()); } } |
- CreateRequestServlet.java 구현 종합
/* * CreateRequestServlet */ public class CreateRequestServlet extends HttpServlet { private static final String SAML_REQUEST_TEMPLATE = "AuthnRequestTemplate.xml"; public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String returnPage = "service_proc.jsp"; // 실제 forwarding수행할 JSP response.setHeader("Content-Type", "text/html; charset=UTF-8"); response.setContentType("text/html; charset=UTF-8"); // get PARAMETERS String ssoURL = request.getParameter("loginForm"); String providerName = request.getParameter("providerName"); String RelayState = request.getParameter("RelayState"); String acsURI = request.getParameter("acsURI"); String SAMLRequest; String redirectURL; try { // create SAMLRequest SAMLRequest = createAuthnRequest(acsURI, providerName); request.setAttribute("authnRequest", SAMLRequest); // compute URL to forward AuthnRequest to the Identity Provider redirectURL = computeURL(ssoURL, SAMLRequest, RelayState); request.setAttribute("redirectURL", redirectURL); } catch (SamlException e) { request.setAttribute("error", e.getMessage()); } request.getRequestDispatcher(returnPage).include(request, response); } } |
- AuthnRequestTemplate.xml 참조
<?xml version="1.0" encoding="UTF-8"?> <samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="##AUTHN_ID##" Version="2.0" IssueInstant="##ISSUE_INSTANT##" ProtocolBinding="urn:oasis:names.tc:SAML:2.0:bindings:HTTP-Redirect" ProviderName="##PROVIDER_NAME##" AssertionConsumerServiceURL="##ACS_URL##"/> |
3.2.3 service_proc.jsp : Identity Provider로 SAMLRequest를 forward
<%@page import="net.paygate.saml.util.RequestUtil"%> <%@page import="java.net.*"%> <% String error = (String) request.getAttribute("error"); String authnRequest = (String) request.getAttribute("authnRequest"); String redirectURL = (String) request.getAttribute("redirectURL"); %> <html><head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"><title>SAML-based Single Sign-On Service </title></head> <% if (error != null) { %> <body><center><font color="red"><b><%= error %></b></font></center><p> <% } else { if (authnRequest != null && redirectURL != null) { %> <body onload="document.location = '<%=redirectURL%>';return true;"> <h1 style="margin-bottom:6px">Submitting login request to Identity provider</h1> <% } else { %> <body> <center><font color="red"><b>no SAMLRequest or redirectURL</b></font></center><p> <% } } %> </body></html> |
3.3 SAMLRequest를 받고 유저인증 진행 단계
이 단계는 Identity Provider에서 진행하게 된다.3.3 SAMLRequest를 받고 유저인증 진행 단계
유저인증은 각 Identity Provider별로 다양한 방법을 취할 수 있고 여기서는 LDAP 인증을 사용하고 있다.
3.3.1 login_form.jsp : 기본적인 유저 인증 정보를 입력받는다.
3.3.1 login_form.jsp : 기본적인 유저 인증 정보를 입력받는다.
- Parameter를 받고
String RelayState = request.getParameter("RelayState");
- SAMLRequest를 Parsing한다.
String[] samlRequestAttributes = ProcessResponseServlet.getRequestAttributes(requestXmlString);
- 사용자 이름과 비밀번호를 입력받고
<input type="password" name="password" id="password" size="18">
- IdentityProviderForm을 생성한다.
....
</form>
- login_form.jsp 구현 종합
<% String SAMLRequest = request.getParameter("SAMLRequest"); String RelayState = request.getParameter("RelayState"); String ServiceProvider = ""; if (SAMLRequest == null || SAMLRequest.equals("null")) { ServiceProvider = ""; } else { String requestXmlString = ProcessResponseServlet.decodeAuthnRequestXML(SAMLRequest); String[] samlRequestAttributes = ProcessResponseServlet.getRequestAttributes(requestXmlString); String issueInstant = samlRequestAttributes[0]; ServiceProvider = samlRequestAttributes[1]; String acsURL = samlRequestAttributes[2]; } %> <html><head><title>SSO Login Page</title></head> <body> <h1><%=ServiceProvider%> Service Login</h1> <form name="IdentityProviderForm" action="https://api.paygate.net/t/sso/saml/ProcessResponseServlet.jsp" method="post"> <input type="hidden" name="SAMLRequest" value="<%=SAMLRequest%>"/> <input type="hidden" name="RelayState" value="<%=RelayState%>"/> <input type="hidden" name="returnPage" value="./login_proc.jsp"> username : <input type="text" name="username" id="username" size="18"> <br> password : <input type="password" name="password" id="password" size="18"><br> <input type="submit" value="로그인"> </form> </body></html> |
3.4 유저 확인후 SAMLResponse 생성
3.4 유저 확인후 SAMLResponse 생성
3.4.1 ProcessResponseServlet
3.4.1 ProcessResponseServlet
- login_form.jsp에서 parameter를 받는다.
String returnPage = request.getParameter("returnPage");
String username = request.getParameter("username");
String password = request.getParameter("password");
String RelayState = request.getParameter("RelayState");
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String SAMLRequest = request.getParameter("SAMLRequest"); String returnPage = request.getParameter("returnPage"); String username = request.getParameter("username"); String password = request.getParameter("password"); String RelayState = request.getParameter("RelayState"); ... } |
- SAMLRequest를 parsing한다.
public static String decodeAuthnRequestXML(String encodedRequestXmlString) throws SamlException { try { Base64 base64Decoder = new Base64(); byte[] xmlBytes = encodedRequestXmlString.getBytes("UTF-8"); byte[] base64DecodedByteArray = base64Decoder.decode(xmlBytes); try { Inflater inflater = new Inflater(true); inflater.setInput(base64DecodedByteArray); byte[] xmlMessageBytes = new byte[5000]; int resultLength = inflater.inflate(xmlMessageBytes); if (!inflater.finished()) { throw new RuntimeException("didn't allocate enough space to hold decompressed data"); } inflater.end(); return new String(xmlMessageBytes, 0, resultLength, "UTF-8"); } catch (DataFormatException e) { ByteArrayInputStream bais = new ByteArrayInputStream(base64DecodedByteArray); ByteArrayOutputStream baos = new ByteArrayOutputStream(); InflaterInputStream iis = new InflaterInputStream(bais); byte[] buf = new byte[1024]; int count = iis.read(buf); while (count != -1) { baos.write(buf, 0, count); count = iis.read(buf); } iis.close(); return new String(baos.toByteArray()); } } catch (UnsupportedEncodingException e) { throw new SamlException("Error decoding AuthnRequest: Check decoding scheme - " + e.getMessage()); } catch (IOException e) { throw new SamlException("Error decoding AuthnRequest: Check decoding scheme - " + e.getMessage()); } } |
- SAMLRequest에서 Attribute을 발췌한다.
String issueInstant = samlRequestAttributes[0];
String providerName = samlRequestAttributes[1];
String acsURL = samlRequestAttributes[2];
public static String[] getRequestAttributes(String xmlString) throws SamlException { Document doc = Util.createJdomDoc(xmlString); if (doc != null) { String[] samlRequestAttributes = new String[3]; samlRequestAttributes[0] = doc.getRootElement().getAttributeValue("IssueInstant"); samlRequestAttributes[1] = doc.getRootElement().getAttributeValue("ProviderName"); samlRequestAttributes[2] = doc.getRootElement().getAttributeValue("AssertionConsumerServiceURL"); return samlRequestAttributes; } else { throw new SamlException("Error parsing AuthnRequest XML: Null document"); } } |
- User 인증을 진행한다.
private boolean login(String username, String password) { LdapLoginHandler ldaplh = new LdapLoginHandler(); if (password.length() < 1) return false; if (ldaplh.isValidOfficeUser(username, password)) { return true; } else { return false; } } |
- RSA Keypair loading
String privateKeyFilePath = keysDIR + "paygate_private.der";
RSAPrivateKey privateKey = (RSAPrivateKey) Util.getPrivateKey(privateKeyFilePath, "RSA");
RSAPublicKey publicKey = (RSAPublicKey) Util.getPublicKey(publicKeyFilePath, "RSA");
- SAMLResponse의 유효기간정보 설정, 현시점부터 24시간 유효하게 설정함
long nowafter = now + 1000*60*60*24;
long before = now - 1000*60*60*24;
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");
java.util.Date pTime = new java.util.Date(now);
String notBefore = dateFormat1.format(pTime);
java.util.Date aTime = new java.util.Date(nowafter);
String notOnOrAfter = dateFormat1.format(aTime);
request.setAttribute("notBefore", notBefore);
request.setAttribute("notOnOrAfter", notOnOrAfter);
- 인증을 거친 로그인 유저네임과 유효기간을 포함한 전자서명전의 SAMLResponse XML data생성
private String createSamlResponse( String authenticatedUser, String notBefore, String notOnOrAfter) throws SamlException { String filepath = getServletContext().getRealPath("t/sso/saml/templates/" + samlResponseTemplateFile); String samlResponse = Util.readFileContents(filepath); samlResponse = StringUtils.replace(samlResponse, "##USERNAME_STRING##", authenticatedUser); samlResponse = StringUtils.replace(samlResponse, "##RESPONSE_ID##", Util.createID()); samlResponse = StringUtils.replace(samlResponse, "##ISSUE_INSTANT##", Util.getDateAndTime()); samlResponse = StringUtils.replace(samlResponse, "##AUTHN_INSTANT##", Util.getDateAndTime()); samlResponse = StringUtils.replace(samlResponse, "##NOT_BEFORE##", notBefore); samlResponse = StringUtils.replace(samlResponse, "##NOT_ON_OR_AFTER##", notOnOrAfter); samlResponse = StringUtils.replace(samlResponse, "##ASSERTION_ID##", Util.createID()); return samlResponse; } |
* getDateAndTime()은 SAML date format에 맞게 날짜시간정보를 생성하는 함수임.
* SAML date format : yyyy-MM-ddThh:mm:ssZ (예: 2008-01-30T23:05:23Z)
* 시간정보는 Localtime이 아닌 UTC에 맞춰줘야함
- SAMLResponse에 대하여 전자서명
* 이때 publicKey는 SAMLResponse message에 포함되지만 데이터 암호화에는 사용되지 않는다.
* xmldsig.jar, xmlsec.jar가 필요함
- 서명된 SAMLResponse를 포함하여 ACS로 forwarding한다.
request.getRequestDispatcher(returnPage).include(request, response);
- ProcessResponseServlet.java 구현 종합
/* * ProcessResponseServlet */ public class ProcessResponseServlet extends HttpServlet { private final String keysDIR = System.getProperty("PGV3_HOME") + SystemUtils.FILE_SEPARATOR + "SSO" + SystemUtils.FILE_SEPARATOR + "keys" + SystemUtils.FILE_SEPARATOR; private final String samlResponseTemplateFile = "SamlResponseTemplate.xml"; private static final String domainName = "paygate.net"; public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String SAMLRequest = request.getParameter("SAMLRequest"); String returnPage = request.getParameter("returnPage"); String username = request.getParameter("username"); String password = request.getParameter("password"); String RelayState = request.getParameter("RelayState"); boolean continueLogin = true; if (SAMLRequest == null || SAMLRequest.equals("null")) { continueLogin = false; request.setAttribute("error","ERROR: Unspecified SAML parameters."); request.setAttribute("authstatus","FAIL"); } else if (returnPage != null) { try { String requestXmlString = decodeAuthnRequestXML(SAMLRequest); String[] samlRequestAttributes = getRequestAttributes(requestXmlString); String issueInstant = samlRequestAttributes[0]; String providerName = samlRequestAttributes[1]; String acsURL = samlRequestAttributes[2]; boolean isValiduser = login(username, password); // 유저인증 if (!isValiduser) { request.setAttribute("error", "Login Failed: Invalid user."); request.setAttribute("authstatus","FAIL"); } else { request.setAttribute("issueInstant", issueInstant); request.setAttribute("providerName", providerName); request.setAttribute("acsURL", acsURL); request.setAttribute("domainName", domainName); request.setAttribute("username", username); request.setAttribute("RelayState", RelayState); String publicKeyFilePath = keysDIR + "paygate_public.der"; String privateKeyFilePath = keysDIR + "paygate_private.der"; RSAPrivateKey privateKey = (RSAPrivateKey) Util.getPrivateKey(privateKeyFilePath, "RSA"); RSAPublicKey publicKey = (RSAPublicKey) Util.getPublicKey(publicKeyFilePath, "RSA"); long now = System.currentTimeMillis(); long nowafter = now + 1000*60*60*24; long before = now - 1000*60*60*24; SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'"); java.util.Date pTime = new java.util.Date(now); String notBefore = dateFormat1.format(pTime); java.util.Date aTime = new java.util.Date(nowafter); String notOnOrAfter = dateFormat1.format(aTime); request.setAttribute("notBefore", notBefore); request.setAttribute("notOnOrAfter", notOnOrAfter); if (!validSamlDateFormat(issueInstant)) { continueLogin = false; request.setAttribute("error", "ERROR: Invalid NotBefore date specified - " + notBefore); request.setAttribute("authstatus","FAIL"); } else if (!validSamlDateFormat(notOnOrAfter)) { continueLogin = false; request.setAttribute("error", "ERROR: Invalid NotOnOrAfter date specified - " + notOnOrAfter); request.setAttribute("authstatus","FAIL"); } if (continueLogin) { // 서명전의 SAMLResponse Message 생성 String responseXmlString = createSamlResponse(username, notBefore, notOnOrAfter); // SAMLResponse에 대한 전자서명 String signedSamlResponse = SAMLSigner.signXML(responseXmlString, privateKey, publicKey); request.setAttribute("samlResponse", signedSamlResponse); request.setAttribute("authstatus","SUCCESS"); } else { request.setAttribute("authstatus","FAIL"); } } } catch (SamlException e) { request.setAttribute("error", e.getMessage()); request.setAttribute("authstatus","FAIL"); } } // Forward SAML response to ACS response.setContentType("text/html; charset=UTF-8"); request.getRequestDispatcher(returnPage).include(request, response); } } |
- SamlResponseTemplate.xml 참조
<samlp:Response ID="##RESPONSE_ID##" IssueInstant="##ISSUE_INSTANT##" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> <samlp:Status> <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> </samlp:Status> <Assertion ID="##ASSERTION_ID##" IssueInstant="2003-04-17T00:46:02Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"> <Issuer>https://www.opensaml.org/IDP </Issuer> <Subject> <NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress"> ##USERNAME_STRING## </NameID> <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/> </Subject> <Conditions NotBefore="##NOT_BEFORE##" NotOnOrAfter="##NOT_ON_OR_AFTER##"> </Conditions> <AuthnStatement AuthnInstant="##AUTHN_INSTANT##"> <AuthnContext> <AuthnContextClassRef> urn:oasis:names:tc:SAML:2.0:ac:classes:Password </AuthnContextClassRef> </AuthnContext> </AuthnStatement> </Assertion> </samlp:Response> |
3.5 SAMLResponse Message를 Service Provider의 ACS로 forward
3.5.1 login_proc.jsp : forwarding을 담당
3.5 SAMLResponse Message를 Service Provider의 ACS로 forward
3.5.1 login_proc.jsp : forwarding을 담당
<%@ page contentType="text/html; charset=UTF-8"%> <% String acsURL = (String) request.getAttribute("acsURL"); String samlResponse = (String) request.getAttribute("samlResponse"); String RelayState = (String) request.getAttribute("RelayState"); String authstatus = (String) request.getAttribute("authstatus"); if (authstatus == null) authstatus = "FAIL"; if (RelayState == null) RelayState = ""; %> <html> <head> <title>forward to ACS</title> </head> <% if (samlResponse != null && authstatus.equals("SUCCESS")) { %> <body onload="javascript:document.acsForm.submit();"> <form name="acsForm" action="<%=acsURL%>" method="post"> <div style="display: none"> <textarea rows=10 cols=80 name="SAMLResponse"><%=samlResponse%></textarea> <textarea rows=10 cols=80 name="RelayState"><%=RelayState%></textarea> </div> </form> <% } else { %><script>alert('Login error'); history.back(-2); </script><% } %> </body> </html> |
3.6 Service Provider이 ACS에서 SAMLResponse를 verify
3.6.1 PublicACSServlet : Service Provider측의 ACS
3.6 Service Provider이 ACS에서 SAMLResponse를 verify
3.6.1 PublicACSServlet : Service Provider측의 ACS
- Parameter를 받고
String RelayState = request.getParameter("RelayState");
- Identity Provider의 public Key를 load
publicKey = (RSAPublicKey) Util.getPublicKey(publicKeyFilePath,"RSA");
- Identity Provider가 보내온 SAMLResponse를 서명검증(verify)
- 검증을 거친이후 실제 서비스 사이트로 forward 요청
- PublicACSServlet.java 구현 종합
public class PublicACSServlet extends HttpServlet { private final String keysDIR = System.getProperty("PGV3_HOME") + SystemUtils.FILE_SEPARATOR + "CryptoServer" + SystemUtils.FILE_SEPARATOR + "keys" + SystemUtils.FILE_SEPARATOR; public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String SAMLResponse = request.getParameter("SAMLResponse"); String RelayState = request.getParameter("RelayState"); // acs knows public key only. String publicKeyFilePath = keysDIR + "paygate_public.der"; RSAPublicKey publicKey; try { publicKey = (RSAPublicKey) Util.getPublicKey(publicKeyFilePath,"RSA"); boolean isVerified = SAMLVerifier.verifyXML(SAMLResponse, publicKey); if (isVerified) { String loginid = null; Document doc = Util.createJdomDoc(SAMLResponse); Iterator itr = doc.getDescendants(); itr = doc.getDescendants(new ElementFilter()); while (itr.hasNext()) { Content c = (Content) itr.next(); if (c instanceof Element) { Element e = (Element) c; System.out.println("Element:" + e.getName()); if ("NameID".equals(e.getName())) { loginid = e.getText().trim(); break; } } } request.setAttribute("mid", loginid); request.setAttribute("RelayState", RelayState); response.setContentType("text/html; charset=UTF-8"); request.getRequestDispatcher("./acs_proc.jsp").include(request, response); } else { System.out.println("SAMLResponse is modified!!"); return; } } catch (SamlException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } } |
3.7 완전하게 동작하는 WAR file
- 화면상의 제약으로 인해 완전하게 동작하는 WAR File을 별도 준비하였다.
- 파일이 필요한 분은 dev@paygate.net으로 메일요청하십시오.
- 또한 아래 링크를 통해서 file download 위치를 확인할 수 있다.
http://docs.google.com/Doc?id=dcxqxct2_4dsh8w4dt
4. SAML SSO 적용 실제 사이트
4. SAML SSO 적용 실제 사이트
4. SAML SSO 적용 실제 사이트
4. SAML SSO 적용 실제 사이트
4.1 PayGate Admin Login
4.1 PayGate Admin Login
4.1.1 최초 Service Login Page
- 이 단계는 SAMLRequest를 생성하기전 서비스 제공자가 제시하는 화면이다.
4.1.2 Identity Provider로서 Login Form 제시
- 이 화면이 보이기까지 서비스 제공자는 SAMLRequest를 생성하고
- Identity Provider로 SAMLRequest를 redirect하면
- 유저인증을 위해서 보여지는 화면이다.
- 이 화면 이후 로그인 버튼을 누르게 되면 SAMLResponse 메세지를 생성하여 Service Provider의 ACS Site로 forwarding하게 된다.
4.1.3 Service Provider ACS verify를 거친후 마지막 페이지
- Service Provider에 위치하는 ACS가 SAMLResponse를 verify한 이후 실제 서비스 제공 사이트로 forwarding한 결과이다.
4.2 Google Apps Service
- google은 service provider역할을 하며
- paygate는 identity provider역할을 한다.
4.2.1 gmail 서비스 로그인을 위한 인증 화면
- google에서 SAMLRequest를 생성하여 identity provider인 페이게이트로 redirect하면
- 페이게이트에서 유저 인증을 위하여 생성한 화면이다.
4.2.2 identity provider의 인증을 거친후 로그인된 화면
4.2.2 identity provider의 인증을 거친후 로그인된 화면
- Identity Provider가 SAMLResponse를 생성하여
- google acs로 forwarding하면 verify한 이후 실제 서비스 제공 사이트로 연결한다.
5. SAML의 확장
5.1 공인인증기관과 Identity Provider
5. SAML의 확장
5. SAML의 확장
5.1 공인인증기관과 Identity Provider
5.1 공인인증기관과 Identity Provider
SAML SSO는 국내에서는 공인인증기관이 Identity Provider역할을 한다면 아주 적당한 business model이 될것 같다.
공인인증기관의 신뢰성을 바탕으로 인증서 발행한 유저에 대한 확인을 직접 수행하고
Service Provider는 공인인증기관의 확인만을 검증하여 그 결과를 신뢰할 수 있다.
더 나아가서는 꼭 공인인증기관이 Identity Provider역할을 하지 않더라도
공인인증서 기반의 Public Identity Provider는 쉽게 예상할 수 있다.
5.2 Payment Gateway와 Identity Provider
결제대행사(Payment Gateway)사가 Identity Provider역할을 하고
쇼핑몰이 Service Provider가 되는 구조를 생각해볼 수 있다.
SAMLRequest는 충분히 확장가능한 구조이므로 Payment 요청에 필요한 필수정보 (상품명, 가격 등)을 포함하여
PG사에 대하여 Identity Provider로서 요청하고
PG사 사이트에서 안전하게 Payment를 처리하고 그 결과를 SAMLResponse format으로 돌려주는 구조이다.
이는 Cross Domain간에 Payment Protocol에 대한 표준이 부재한 현 상황에서
의미있게 시도해봄직한 목표이다.
6. 참고자료
SAML Single Sign On Service for Google Apps
OpenSAML
SAML at Wikipedia
출처 : 한국썬 개발자 네트워크 블로그
기고자: 페이게이트 이동산 이사 mountie@paygate.net
'개발 이야기 > Java' 카테고리의 다른 글
[JavaFX] 선언적 사용자 인터페이스 : 학습 곡선 일지 2편 (0) | 2008.09.25 |
---|---|
[JavaFX] 스크립트 탐구 : 학습 곡선 일지 1편 (0) | 2008.09.25 |
SVN 서버 설치 및 거북이(Tortoise) SVN 설치하기 (0) | 2008.03.13 |
@Override 사용하기 (0) | 2008.01.24 |
Generic 사용하기..*^^* (0) | 2008.01.23 |