Статьи

ColdFusion и OAuth, часть 3 — Google

Добро пожаловать в мой третий и последний пост в блоге о ColdFusion и OAuth. (Вы можете найти более ранние записи ниже.) Я извиняюсь за задержку, но я путешествовал на прошлой неделе, поэтому я немного отстал. Если вы еще не прочитали предыдущие записи ( часть 1 и часть 2 ), сделайте это, поскольку я не буду повторять информацию, о которой писал ранее.

Итак — надеюсь, вы уже поняли, насколько простым может быть OAuth. После того, как у меня все заработало в первый раз, второе демо оказалось довольно простым. Для этой демонстрации я решил немного повеселиться. У Google есть OAuth API, который позволяет вам проходить аутентификацию на своих серверах. Что, если вы хотите использовать Google для своей пользовательской системы? Другими словами, пропустите весь пользовательский процесс регистрации и перенесите управление пользователями в Google. Это не только возможно, но и является одним из рекомендуемых вариантов использования в их документации .

Прежде чем мы начнем, обратите внимание, что вам необходимо зарегистрировать свое приложение в Google. Это в точности как то, что вы делали с Facebook и LinkedIn, за исключением того, что вместо этого это делается в консоли API Google . К настоящему времени этот процесс должен быть достаточно легко , где мне не нужно , чтобы объяснить вам, только не забудьте сделать примечание идентификатора клиента и секрет клиента .

Поскольку наше приложение будет использовать Google для входа в систему, я создал простой Application.cfc, который ищет переменную сеанса и, если она не существует, автоматически подталкивает пользователя на страницу входа.

component {
    this.name="googleoauthlogin4";
    this.sessionManagement=true;

    public boolean function onApplicationStart() {
        application.clientid="noyb";
        application.clientsecret="stillnoyb";
        application.callback="http://localhost/testingzone/googleoauthlogin/callback.cfm";

        application.google = new google(application.clientid, application.clientsecret);
        return true;
    }

    public boolean function onRequestStart(required string req) {

      if(!findNoCase("/login.cfm", arguments.req) && !findNoCase("/callback.cfm", arguments.req) && !session.loggedin) {
    		location(url="./login.cfm", addToken="false");
    	}
    	return true;
    }

    public boolean function onSessionStart() {
    	session.loggedin = false;

    	return true;
    }
}

OnApplicationStart является практически точной копией предыдущих примеров, но onRequestStart является новым. Он проверяет, вошли ли мы в систему, и запрещает это, проверяет, запрашиваем ли мы страницу входа или страницу обратного вызова. Когда пользователь впервые нажимает на приложение, они отправляются на страницу входа:

Обратите внимание, что здесь нет формы. Помните, что вместо этого мы отправляем пользователя в Google. Я мог бы автоматически толкнуть их, но я чувствовал, что это было более дружелюбно. Вот код для этого шаблона.

<cfset authurl = application.google.generateAuthUrl(application.callback, session.urltoken)>

<h1>Login Required</h1>

<p>
  In order to use this app, you must login with your Google account. Click to login below.
</p>

<cfoutput>
	<p>
	<a href="#authurl#">LOGIN!</a>
	</p>
</cfoutput>

Я поместил код Google OAuth в CFC, чтобы немного абстрагироваться, но пока не беспокойтесь об этом. Генерация ссылок очень похожа на предыдущие два примера. Как только пользователь нажимает кнопку входа, они отправляются в Google. В этом случае Google распознал мою учетную запись и предварительно установил логин, но у меня также есть возможность переключать пользователей.

Как и прежде, пользователь отправляется на страницу обратного вызова. Вот этот шаблон. Еще раз обратите внимание, что я вложил гораздо больше в CFC, так что это несколько проще.

<!--- Validate the result --->
<cfparam name="url.code" default="">
<cfparam name="url.state" default="">
<cfparam name="url.error" default="">
<cfset result = application.google.validateResult(url.code, url.error, url.state, session.urltoken)>

<cfif not result.status>
  <!--- Imagine a nicer error here. --->
	<cfoutput>
		<h1>Error!</h1>
		#result.message#
	</cfoutput>
	<cfabort>
</cfif>

<cfset session.token = result.token>
<cfset session.loggedin = true>
<cflocation url="index.cfm" addtoken="false">

Наконец, пользователь направляется на домашнюю страницу. Как часть Google API, я могу получить информацию о пользователе. Я так и сделал и выкинул это:

Вот этот шаблон:

<cfset me = application.google.getProfile(session.token.access_token)>

<h1>Home</h1>

<cfdump var="#me#" label="me">

Теперь давайте посмотрим на CFC:

component {

  public function init(clientid, clientsecret) {
		variables.clientid = arguments.clientid;
		variables.clientsecret = arguments.clientsecret;
		return this;
	}

	public string function generateAuthURL(redirecturl, state) {
		/*
		Scope is what you want to do with your access. Since this demo is ONLY for
		auth and user info, we have one hard coded value.
		*/
		return "https://accounts.google.com/o/oauth2/auth?" & 
				 "client_id=#urlEncodedFormat(variables.clientid)#" & 
     			 "&redirect_uri=#urlEncodedFormat(arguments.redirecturl)#" & 
				 "&scope=https://www.googleapis.com/auth/userinfo.profile&response_type=code" & 
				 "&state=#urlEncodedFormat(arguments.state)#";

	}

	public function getProfile(accesstoken) {

		var h = new com.adobe.coldfusion.http();
		h.setURL("https://www.googleapis.com/oauth2/v1/userinfo");
		h.setMethod("get");
		h.addParam(type="header",name="Authorization",value="OAuth #accesstoken#");
		h.addParam(type="header",name="GData-Version",value="3");
		h.setResolveURL(true);
		var result = h.send().getPrefix();
		return deserializeJSON(result.filecontent.toString());
	}

	/*
	I handle validating the code result from Google and automatically getting the auth token.
	I should be able to handle any bad result from Google or the user not allowing crap.
	I also validate the state.
	*/
	public struct function validateResult(code, error, remoteState, clientState) {
		var result = {};

		//If error is anything, we have an error
		if(error != "") {
			result.status = false;
			result.message = error;
			return result;
		}

		//Then, ensure states are equal
		if(remoteState != clientState) {
			result.status = false;
			result.message = "State values did not match.";
			return result;
		}

		var token = getGoogleToken(code);

		if(structKeyExists(token, "error")) {
			result.status = false;
			result.message = token.error;
			return result;
		}
		
		result.status = true;
		result.token = token;

		return result;
	}

	//Credit: http://www.sitekickr.com/blog/http-post-oauth-coldfusion
	private function getGoogleToken(code) {
		var postBody = "code=" & UrlEncodedFormat(arguments.code) & "&";
			 postBody = postBody & "client_id=" & UrlEncodedFormat(application.clientid) & "&";
			 postBody = postBody & "client_secret=" & UrlEncodedFormat(application.clientsecret) & "&";
			 postBody = postBody & "redirect_uri=" & UrlEncodedFormat(application.callback) & "&";
			 postBody = postBody & "grant_type=authorization_code";


			var h = new com.adobe.coldfusion.http();
			h.setURL("https://accounts.google.com/o/oauth2/token");
			h.setMethod("post");
			h.addParam(type="header",name="Content-Type",value="application/x-www-form-urlencoded");
			h.addParam(type="body",value="#postBody#");
			h.setResolveURL(true);
			var result = h.send().getPrefix();
			return deserializeJSON(result.filecontent.toString());

	}

}

По большей части я предполагаю, что это говорит само за себя, но если у кого-то есть вопросы, дайте мне знать

Наконец, интересный поворот. А как насчет компаний, которые используют Google Apps? Оказывается, есть недокументированное решение для этого. Посмотрите на функцию generateAuthURL выше. Если вы добавите аргумент «hd», вы можете указать домен Служб Google:

  public string function generateAuthURL(redirecturl, state) {
		/*
		Scope is what you want to do with your access. Since this demo is ONLY for
		auth and user info, we have one hard coded value.
		*/
		return "https://accounts.google.com/o/oauth2/auth?hd=camdenfamily.com&" & 
				 "client_id=#urlEncodedFormat(variables.clientid)#" & 
     			 "&redirect_uri=#urlEncodedFormat(arguments.redirecturl)#" & 
				 "&scope=https://www.googleapis.com/auth/userinfo.profile&response_type=code" & 
				 "&state=#urlEncodedFormat(arguments.state)#";

	}

Это прекрасно работает, но, как я уже сказал, это не задокументировано. Мой друг является платным клиентом Служб Google и обратился в службу технической поддержки, но, к сожалению, никто не даст ему точного ответа. Его последнее письмо с ними привело к этому:


В основном, по словам представителя, его техническая команда сказала, что использование hd = cbtec.com является функцией, поэтому они не указали это в своей документации.
Он также сказал, что переменная hd также используется для аутентификации пользователей в организации с бизнес-единицами, разделенными доменами.

Честно говоря, это первое предложение не имеет смысла. «Это особенность, и поэтому мы не документировали ее». Я надеюсь, что это была просто опечатка.

Связанные записи в блоге