The Windows Azure Toolkits – Integrated ACS with iOS、Windows Phone、Android and Windows 8 – Secure WCF Service

在前一篇文章中,我們讓Android、iOS、Windows Phone及Windows 8應用程式的使用者可以透過ACS整合Facebook Identity Provider完成使用者初步的帳密驗證動作,但這只是前半部故事,

當完成驗證動作後,接下來該做什麼事呢?

 

/黃忠成

 

 

What’s Next?

 

 

   在前一篇文章中,我們讓Android、iOS、Windows Phone及Windows 8應用程式的使用者可以透過ACS整合Facebook Identity Provider完成使用者初步的帳密驗證動作,但這只是前半部故事,

當完成驗證動作後,接下來該做什麼事呢?

   在透過ACS整合Identity Providers後,應用程式取得的其實只是一串Secure Token,依據Identity Provider及設定,這串Token裡面可能會包含Email或是使用者名稱,但也可能只包含一個

ID(Windows Live ID就只提供一個唯一識別碼),這意味著應用程式通常必須接續下來做一些有意義的事,例如要求使用者輸入EMAIL或送貨地址等個人資料。

   是的,ACS與Identity Providers只提供帳密的驗證機制,相關後續動作還是得由應用程式來處理。

   那當使用者通過驗證,且輸入了應用程式所需要的個人資料後,接下來應用程式該做些什麼?

 

Creating Secure WCF Service

 

   當應用程式設計為要使用者先通過驗證後方能使用時,其接下來的動作必定是透過某些通道與後端Server做溝通。舉個例來說,我們設計了一個購物應用程式,當使用者首次登入後,

應用程式會詢問使用者的一些個人資料(不包括帳密),接著應用程式發出一個Web Service呼叫來將這些個人資料傳送到後端儲存後,應用程式再發出一個Web Service呼叫來取得商品列表。

圖1

就正規設計來說,這個WCF Service必然得設計為需要通過驗證方能呼叫,未經授權的呼叫會被擋在門外,這時由ACS所取得的Secure Token就成為了驗證碼。

  那如何撰寫這樣的WCF Service呢?首先得先安裝Windows Identity Foundation Runtime及Windows Identity Foundation SDK。

 

Windows Identity Foundation Runtime

http://msdn.microsoft.com/en-us/security/aa570351.aspx

Windows Identity Foundation SDK

http://www.microsoft.com/en-us/download/details.aspx?id=4451

 

安裝完成後,建立一個Web Application Project,添加WAToolkitForWP7\Samples\WP7.1\Libraries\DPE.Oauth及Microsof.IdentityModel的Reference,

然後建立WCF Service。

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;

namespace WebApplication15
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IService1" in both code and config file together.
    [ServiceContract]    
    public interface IService1
    {
        [OperationContract]
        [WebInvoke(Method = "GET", UriTemplate = "/HelloWorld", RequestFormat = WebMessageFormat.Json,
           ResponseFormat = WebMessageFormat.Json, BodyStyle=WebMessageBodyStyle.WrappedResponse)]
        string HelloWorld();
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using Microsoft.IdentityModel.Claims;
using System.Web;

namespace WebApplication15
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "Service1" in code, svc and config file together.
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    public class Service1 : IService1
    {
        private bool IsAuthenticated()
        {
            return HttpContext.Current.User.Identity.IsAuthenticated;
        }



        public string HelloWorld()
        {
            if (IsAuthenticated())
                return "hello world.";
            else
                throw new Exception("Error.");
        }
    }
}

 

修改web.config

<?xml version="1.0"?>

<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->

<configuration>
  <configSections>
    <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </configSections>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <!--<httpModules>
      <add name="ProtectedResourceModule" type="Microsoft.Samples.DPE.OAuth.ProtectedResource.ProtectedResourceModule" />
      <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"  />
    </httpModules>-->

  </system.web>
  
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules runAllManagedModulesForAllRequests="true">
      <add name="ProtectedResourceModule" type="Microsoft.Samples.DPE.OAuth.ProtectedResource.ProtectedResourceModule" />
      <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
      <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
    </modules>
    <handlers />
  </system.webServer>
  <system.serviceModel>
      <services>
        <service name="WebApplication15.Service1">
          <endpoint address="" binding="basicHttpBinding"
              contract="WebApplication15.IService1" />
          <endpoint address="json" binding="webHttpBinding"  behaviorConfiguration="jsonBehavior" contract="WebApplication15.IService1"/>
          <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
        </service>
      </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>       
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="jsonBehavior">
          <webHttp/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <microsoft.identityModel>
    <service name="OAuth">
      <audienceUris>
        <add value="http://www.code6421.com"  />
      </audienceUris>
      <securityTokenHandlers>
        <add type="Microsoft.Samples.DPE.OAuth.Tokens.SimpleWebTokenHandler, Microsoft.Samples.DPE.OAuth" />
      </securityTokenHandlers>
      <issuerTokenResolver type="Microsoft.Samples.DPE.OAuth.ProtectedResource.ConfigurationBasedIssuerTokenResolver, Microsoft.Samples.DPE.OAuth">
        <serviceKeys>
          <add serviceName="http://www.code6421.com" serviceKey="<your key>k+PzV04dEGgQ/9/vYP7rKrTAtTDUMhx42IWoLq/----=" />
        </serviceKeys>
      </issuerTokenResolver>
      <issuerNameRegistry type="Microsoft.Samples.DPE.OAuth.ProtectedResource.SimpleWebTokenTrustedIssuersRegistry, Microsoft.Samples.DPE.OAuth">
        <trustedIssuers>
          <add issuerIdentifier="https://demoacs23.accesscontrol.windows.net/" name="demoacs23" />
        </trustedIssuers>
      </issuerNameRegistry>
    </service>
  </microsoft.identityModel>

</configuration>

有三個地方要注意

 

<audienceUris>

        <addvalue="http://www.code6421.com"  />

</audienceUris>

這裡要填入ACS中信賴憑證者應用程式領域的設定。

圖2

 

<addserviceName="http://www.code6421.com"serviceKey="<your key>k+PzV04dEGgQ/9/vYP7rKrTAtTDUMhx42IWoLq/----="/>

這裡要填入憑證的對稱金鑰。

圖3

 

<trustedIssuers>

          <addissuerIdentifier="https://demoacs23.accesscontrol.windows.net/"name="demoacs23"/>

</trustedIssuers>

這裡要填入ACS的網址。

完成後將此Web應用程式部屬到IIS,就算完成一個僅能供擁有ACS所發出的Secure Token應用程式呼叫的WCF Service。

(PS: 注意,我特別把這個WCF Service設定為支援SOAP及REST,後者是為了方便Android/iOS呼叫)。

 

Windows Phone Consumer

 

   接下來修改我們的DemoACS這個Windows Phone應用程式,加入對此WCF Service的Service Reference,然後撰寫呼叫WCF Service的程式碼。

private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
        {
            if (!_rstrStore.ContainsValidRequestSecurityTokenResponse())
            {
                NavigationService.Navigate(new Uri("/SignInControl.xaml", UriKind.Relative));
            }
            else
            {

                ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
                var store = App.Current.Resources["rstrStore"] as SL.Phone.Federation.Utilities.RequestSecurityTokenResponseStore;
                using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
                {
                    var httpRequestProperty = new HttpRequestMessageProperty();
                    httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + store.SecurityToken;
                    OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
                    client.HelloWorldCompleted += (s, args) =>
                    {
                        Dispatcher.BeginInvoke(() =>
                            {
                                MessageBox.Show(args.Result);
                            });
                    };
                    client.HelloWorldAsync();
                }
                textBlock1.Text = "thanks for your login";
            }
        }

當呼叫這個透過Windows Identity Foundation整合,需Secure Token方能呼叫的WCF Service時,呼叫端必須把Secure Token放在HTTP Header中的Authorization區段,如下所示。

var httpRequestProperty = new HttpRequestMessageProperty();     
 httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + 
store.SecurityToken;                   
 OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;

 

一切正常的話,應該可以看到以下結果。

圖4

讀者們可以嘗試不經過驗證直接呼叫此WCF Service,會出現以下畫面。

圖5

 

Android Consumer

 

  由於Android中並沒有很方便的SOAP Toolkit可以快速的使用SOAP來呼叫WCF Service,因此在先前設計這個WCF Service時,我們加入了REST協定,這樣Android就可以

輕易地呼叫這個WCF Servcie了。

 

package com.example.demoacsb;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;

import com.microsoft.samples.windowsazure.android.accesscontrol.core.IAccessToken;
import com.microsoft.samples.windowsazure.android.accesscontrol.login.AccessControlLoginActivity;

import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.view.Menu;
import android.view.MenuItem;
import android.support.v4.app.NavUtils;

public class SuccessLoginActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_success_login);
        IAccessToken accessToken = null;
		Bundle extras = getIntent().getExtras(); 
		if(extras != null) {
			accessToken = (IAccessToken)extras.getSerializable(AccessControlLoginActivity.AuthenticationTokenKey);
			try {
				CallService(accessToken.getRawToken());
			} catch (ClientProtocolException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (JSONException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}  	 
    }
    
    private void CallService(String token) throws ClientProtocolException, IOException, JSONException{
    	DefaultHttpClient client = new DefaultHttpClient();
    	HttpGet request = new HttpGet("http://192.168.1.124/WebSite1/Service1.svc/json/HelloWorld");
    	request.setHeader("Accept", "application/json");
    	request.setHeader("Content-type", "application/json");
    	request.setHeader("Authorization","OAuth " + token );
    	HttpResponse response = client.execute(request);
    	HttpEntity entity = response.getEntity();
    	if(entity.getContentLength() != 0) {
    		Reader jsonReader = new InputStreamReader(response.getEntity().getContent());
    		char[] buffer = new char[(int) response.getEntity().getContentLength()];
    		jsonReader.read(buffer);
    		jsonReader.close();
    		JSONObject jsonValues =  new JSONObject(new String(buffer));
    		String s = jsonValues.getString("HelloWorldResult");
    		Builder MyAlertDialog = new AlertDialog.Builder(this);
    		MyAlertDialog.setTitle("Result");
    		MyAlertDialog.setMessage(s);
    		MyAlertDialog.show();
    	}
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_success_login, menu);
        return true;
    }    
}

圖6

 

 

iOS Consumer

 

  想透過iOS來呼叫REST/JSON的WCF Service,得先安裝一組JSON Framework,可以由以下網址取得。

https://github.com/stig/json-framework

DemoACSViewController.m

- (IBAction)loginAction:(id)sender {
    WACloudAccessControlClient *acsClient = [WACloudAccessControlClient accessControlClientForNamespace:ACSNamespace realm:ACSRealm];
    [acsClient showInViewController:self allowsClose:NO withCompletionHandler:^(BOOL authenticated) { 
        if (!authenticated) { 
            UIAlertView *dialog = [[UIAlertView alloc] initWithTitle:@"Info" message:@"Login Fail" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [dialog show];
        } else {
            NSURL *url = [NSURL URLWithString:@"http://192.168.1.124/WebSite1/Service1.svc/json/HelloWorld"];
            NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
            NSString *oauthHeader = [[NSString alloc] initWithString:@"OAuth "];
            NSString *s = [[NSString alloc] initWithString:[oauthHeader stringByAppendingString:[[WACloudAccessControlClient sharedToken] securityToken]]];
            [request setValue:s forHTTPHeaderField:@"Authorization"];
            NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
            
        }
    }];    
    
}

-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSDictionary *returnData = [jsonString JSONValue];
     UIAlertView *dialog = [[UIAlertView alloc] initWithTitle:@"Info" message:[returnData objectForKey:@"HelloWorldResult"] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [dialog show];
}

 

圖7

 

Windows 8 Consumer

 

  Windows 8的寫法與Windows Phone差不多,如下。

 

async void login_OnLogin(object sender, LoginEventArgs e)
        {
            ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
            using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
            {
                var httpRequestProperty = new HttpRequestMessageProperty();
                httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + e.Result.Token;
                OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
                new MessageDialog(await client.HelloWorldAsync()).ShowAsync();
            }
        }

圖8