ひさしぶり

ここのところ仕事的にも人生的にも忙しく、ここに何もかけずにいました。が、ちょっと仕事でやっていることを個人ブログであるここに書いていい流れができてきたので、またチョボチョボとここにも色々書いていきたく思います。

しかし、はてなダイアリー久しぶりなので、はてな記法とか全然覚えてない。 markdownとかつかえないのかな。

Personiumの紹介

まず手始めにPersoniumの簡単な紹介から。ペルソニアムと読みます。

http://personium.io
http://github.com/personium/io

このソフトウェアは2008年ごろ、自分が会社の研究所にいたときに「新たなITの形を考えてみよう」という話があったときに、思いついたアイディアから始まりました。今PDS(Personal Data Store)という名前がそこそこ広まって来ましたが、まさにそのアイディアです。そのアイディアを思いついたときは「わっ、すげーこと思いついた」と興奮したのを覚えています。でも、世の中「新たなことを思いついた」と思っても大抵は先人がすでに思いついているもので、これも例にもれずPDSという考え自体は2000年代初頭からあったようですね。

そんなことは露知らず「このアイディアを形にしたい」という想いでプロトタイプをつくり、PDSという言葉もBaaSという言葉もなかったので、このソフトウェアは何であるということを説明するのに苦労しながらも仕事として試作や実証などをさせてもらい、このソフトウェアを成長させることができました。

PDSなのかBaaSなのか

個人的にPDSPDSである前にBaaS(Backend as a Service)でなくてはならないという風に思っています。なぜならば、今、個人というものはスマホタブレット・PC・ゲーム機・センサなど様々な端末で、実に様々なアプリを操作しながら自分のデータというものを生成しつづけているからです。iOSやらAndroidやらWindowsやらと端末のプラットフォームを問わずにデータを入れてもらうためには、RESTという文化がフィットすると思ってます。HTTP(S)がしゃべれないプラットフォームはないですし、RESTという考え方はデータ指向のものだからです。

PDSという世界は、ともすれば既存のICTの在り方を根本から変えませんかという野心的な試みなので、今現在、なかなかそれ単体では「実用」になりづらい研究的な世界のものかと思っています。一方でBaaSというのはモバイル・タブレットアプリをつくるということならばすぐに役に立つ技術です。ですので、会社の仕事としてこれをやってきた関係上、BaaSとしての側面を重点的に磨いてきています。

ODataサポート

具体的にはMicrosoftさんが主導してOASIS標準にしたODataというRESTベースのプロトコルのサポートが一つの強みになっています。ODataはリレーショナルデータの操作を(SQLでなく)RESTでやるためのプロトコルで、すごくよくできた仕様だと思います。

http://www.odata.org/

personiumは、アプリケーションが扱うデータというものは「木構造ディレクトリ構造をもったファイルシステム的なもの」か「リレーショナルデータ的なもの」の2種類でほぼ事足りるかなぁという仮説に立っています。それで、personiumのアプリケーション毎のデータ空間はWebDAVの空間なのですが、そこにODataの空間が切れるようにして2種類のデータを扱えるようにしています。

実は2009年ごろにつくった初代のpersonium実装では、ODataに似た仕組みを自分で考案して実装していたのですが、機能が非常に貧弱で掲示板がやっと作れるかどうかというような代物でした。ちょうどそのころ、良いタイミングでMSさんがODataを発表されました。研究所の同僚がそれを見つけて教えてくれて、これはいいと思って見よう見まねで2010年ごろの実装で取り入れました。ODataは結構高機能で複雑な仕様なので、最初はエッセンスだけ取り入れる感じでしたが、2012ごろの改修時に比較的ちゃんとこの仕様を実装できました。

OASIS標準になったのはOData V4になったときですが、我々がOData実装を取り込んだのがV2の時点だったので現状のpersoniumはOData V2のサポートにとどまっています。OData V4ではV2では仕様化されていなかったSQLでいうところのsum()やらavg()に集約関数などが使えるようになっていたりするので、どこかでOData V4のサポートもできたらいいなと思っています。

復活

5年ぐらい間が空きましたが、久しぶりにこのBlogを更新してゆこうと思います。理由は、先日仕事でやっている以下のオープンソースPDS/BaaSソフトウェアを公開し、会社の方針として個人Blogを通じて世の中の技術者の皆さんに草の根的に色々伝えてゆくべしということとなったためです。

http://personium.io
http://github.com/personium/io

このソフトウェア、色々なところですでに使われているのですが、今のところドキュメント類が少ないのが非常に弱点になってます。ので、それを補う役割も担えたらなと思ってます。

ThriftとかProtocolBufferとか

自分はREST信者だけど、昨日、「RESTに固執せずThriftとかどうよ」という話をもらった。Thriftとか名前以外あんまり知らなかったので、一応20分ほどWebで調べてみた。結論からいうと、「やっぱりあまり興味を持てない」と思った。

ThriftとかProtocol Bufferのいいところって、

  1. ネットワーク転送量的にオーバーヘッドが少ない
  2. いろんな言語ですぐに使える
  3. HTTPベースのRPC
  4. 色々なサービスの内部で使われることはもちろん、EvernoteAPIとかでも使われている。

ってところだと理解した。が、とりあえずあまり魅力は感じなかった。なぜか。

  1. 1を突きつけられる場面では確かにRESTは多少分が悪い。がXMLの冗長性を攻撃するのアプローチはどうかとおもう。ってのは、XMLが冗長なデータフォーマットであることは登場したときから最初からわかってることだから。パフォーマンスがクリティカルな局面であんな形式の電文を生で流すのはみんな「最初からありえない」前提だったわけでしょ?それを今更なにいってんのと。異文化(業種とか)間相互接続性(名前空間とか)が求められる世界ではXML、異文化相互接続性が不要なら人間にとってのそこそこの可読性を保てるJSON、で、パフォーマンスのため転送量減らしたければ、TransferEncoding: gzip。という形でRESTやっていれば、そこそこ困らない。(パレートの法則的に)
  2. HTTPの接続・切断のオーバーヘッドが気になるなら、KeepAliveすればいい。
  3. そもそもRPC vs Data という話で、あんまりRPCが必要な場面が個人的にはない。
  4. RPCが必要になる場面においても、CORBAやらなにやら、XMLより昔からある"効率的な"RPCと何が違うのかもよくわからない。
  5. ThriftやらProtocol Buffer実際やるとなったら、独自の定義体書くためにはそれを学習しなくてはならないし、細かい話、コンパイルするオペレーションをCIに組み込むにはどうすべきかなど、色々付随する作業が出てくる。その直観的コスト見積もりと、見返りである効果を考えたとき、今のところあまり魅力を感じない。

まあ、全く興味がないというと嘘になって、今、RubyJavaの混在実装を扱ったりしていたりするので、実はふつうにRPCで実装するといいのかもしれない。けど、そもそもまだまだHTTPを使いこなせて(非同期IOとかWebSocketとか)いない今、そっちに時間投資するより、ちゃんとHTTPが使いこなせるようになりたいなと思う。

そんな感じ。まあ、FBやGoogle発だからブランド的にみんなが飛びついて普及するということなら、僕もいずれやるかもな。。っていうか、時間さえあれば、やってみたいとは、思う。

参考:
http://ohnaka.jp/blog/2011/08/493
http://blog.broomie.net/index.cgi?id=38

http://code.google.com/intl/ja/apis/protocolbuffers/
http://code.google.com/intl/ja-JP/apis/protocolbuffers/docs/faq.html

CouchDB 1.0.3からのバグ

CouchDB 1.0.3 以降で発生するバグを仕事の仲間が発見しました。ひとことでいうと、
「削除されたドキュメントのあるDBをレプリケートしたときに、同じキーでドキュメントを作ろうとすると201が返るが実際は作成されない。」
というもの。1.0.1, 1.0.2 ではこの現象は発生せず、1.0.3, 1.1.0では発生します。

再現手順

    • 任意のDB(db1とする)のなかの任意のドキュメント(doc1)を削除する
    • 新たなDB(db2とそる)をつくり、db1からレプリケーションを行う。
    • レプリケートであるdb2にdoc1と同じキーで新たなドキュメント(doc2)をPUTする。
      • ⇒201 Createdが返ってくる。
    • db2のdoc2をGETする。
      • ⇒(1.0.2以前)ちゃんとさほどいれたドキュメントが返ってくる(あるべき振る舞い)
      • ⇒(1.0.3以降)404が返ってくる。(たぶんバグ)

英語でバグレポートをあげたいねと話をしているところ。

URL中のPath部分のパーセントエンコード

URLのPath部分でURLエンコードした文字列を扱いたい!

Apacheの裏でPassenger動かしている環境において、パス部分にパーセントエンコードされた文字列を含む、たとえば以下のようなURLを扱おうとすると、問題に直面する。

http://example.com/foo/http%3A%2F%2Fexample.org%2Fbar/baz

何がおこるかというと、Railsにわたる前にApacheが勝手にパーセントデコードしてしまうため、
Railsのroutes.rbで受ける段階で、たとえば以下のような記述をしてもちゃんと意図した形でマッチがなされないためだ。

match '/foo/:url/baz' => ..

この問題の解決のため(解決にはなっていないのだが)、泣く泣くサービスの外部仕様を捻じ曲げて、扱うURLの中で、以下のようにパーセントエンコードを2重でかけるという手段を使ってきた。

http://example.com/foo/http%253A%252F%252Fexample.org%252Fbar/baz

こうすることで、Apacheの野郎が余計なお世話としてパーセントデコードを1回してくれやがったとしても、Rails にはパーセントエンコードされた文字列(たとえば上記の例でいえば:urlのところにhttp%3A%2F%2Fexample.org%2Fbarという文字列)がわたってくるので、これをアプリ側でデコードすることで、意図した機能をなんとか実現することはできていた。ただしこのやり方はサービスの外部仕様であるURLが無駄に長く汚くなるという問題が残る。

ApacheからNginx に乗り換えたら・・・解決するかとおもったが、しなかった。

で、先日WebサーバをApacheからnginxに置き換えたところ、同じURLの使い方(2重エンコード)のリクエストで、Railsに2重エンコードされたままの文字列が返ってくるようになった。NGINXは余計なお世話のデコードをしないのかと思いと一瞬期待したのだが、結局期待ははずれた。

何がおこったかというと、Nginxは2重エンコードされた部分はそのまま放置するくせに、ふつうに1回パーセントエンコードされたパスはApache同様余計なデコードをしやがってくれた。すなわち、%25は%にデコードせずそのまま%25で渡してくるのに、%2Fはしっかり/にデコードしやがるのだ。

URLエンコードってそもそもRFC的にどうなのさ?

Apacheといい、NGINXといい、なんでこういう余計なお世話をしやがってくれるのか? 気になりRFCをあさってみた。すなわち「これバグじゃねぇ?」と言いたいのだが、そもそもURL中、Path部分でパーセントエンコードされた文字をどういう風に扱うべきか、仕様をしらないと自信をもって「バグだ」といえないからである。

まず、URLってのは今はRFC3986に従うのが正しいようだ。
http://d.hatena.ne.jp/keisukefukuda/20080321/p1



ApacheにしろNGINXにしろ、あの実装は「Path部分でPercent-Encodeされた文字列は、サーバが勝手にデコードしてしまっていい」と主張しているのと等しい。
その主張が正当性を得るためのシナリオとして「URL正規化で結局エンコード前と同等URLとみなされるべきだから」というのが想像されたので、僕はそうじゃないことを確認したいわけだ。つまり僕の関心事は、section6.2のあたりになる。

で、見つけたのが、6.2.2.2の記述

   In addition to the case
   normalization issue noted above, some URI producers percent-encode
   octets that do not require percent-encoding, resulting in URIs that
   are equivalent to their non-encoded counterparts.  These URIs should
   be normalized by decoding any percent-encoded octet that corresponds
   to an unreserved character, as described in Section 2.3.

http://tools.ietf.org/html/rfc3986#section-6.2.2.2


本来Percent Encodeの必要のないunreserved characterをPercent EncodeするURI生成機構があるということと、そういうURI生成機構で生成されたunreserved characterに対応するpercent-encodeされたオクテットを含むURLは、その部分をデコードする形で正規化されるということが書いてある。

すなわち(ちゃんとそうは書いていないが)ここから読み取れるのは、unreserved でないreservedなcharacterをPercent EncodeしたURLは、正規化しても元のURLと同等にはならないということだ。よし! そしてunreserved characterとは以下の定義である。

unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"

http://tools.ietf.org/html/rfc3986#section-2.3

つまり/や:はreservedなわけで、percent-encodeされたものとencode前のものは正規化しても別URLになるということだ。例を挙げると、以下において1と2は同じだが、2と3は違うURLとして扱うべきということとなる。

1. http://example.com/foo/http%3A%2F%2Fexample%2Eorg%2Fbar/baz
2. http://example.com/foo/http%3A%2F%2Fexample.org%2Fbar/baz
3. http://example.com/foo/http:/example.org/bar/baz

ということで、やっぱりApacheもNginxも勝手にReserved characterをデコードするのはやめてほしいもんだし、
そういう動きは「バグだ!」といっちゃっていいんだと思う。

URLのパス部分に使っていい文字ってなんだろう?

一応念のためURLのパス部分ってどういう文字を使ってよいものなのか、rfc3986から関連するsyntax定義をひろってみた。

      URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ]

      hier-part   = "//" authority path-abempty
                  / path-absolute
                  / path-rootless
                  / path-empty
  scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
   authority   = [ userinfo "@" ] host [ ":" port ]
    path          = path-abempty    ; begins with "/" or is empty
                    / path-absolute   ; begins with "/" but not "//"
                    / path-noscheme   ; begins with a non-colon segment
                    / path-rootless   ; begins with a segment
                    / path-empty      ; zero characters

      path-abempty  = *( "/" segment )
      path-absolute = "/" [ segment-nz *( "/" segment ) ]
      path-noscheme = segment-nz-nc *( "/" segment )
      path-rootless = segment-nz *( "/" segment )
      path-empty    = 0

      segment       = *pchar
      segment-nz    = 1*pchar
      segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
                    ; non-zero-length segment without any colon ":"

      pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
   pct-encoded   = "%" HEXDIG HEXDIG

   unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
   reserved      = gen-delims / sub-delims
   gen-delims    = ":" / "/" / "?" / "#" / "[" / "]" / "@"
   sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
                 / "*" / "+" / "," / ";" / "="

なるほど。面白い。パスの部分に:とか@とか=使っていいんだね。そうすると、以下のような変態的URLも「あり」ってこと。

http://example.com/unreserved-._~/percentenc%2527%3A%2F/subdelims!$&'()*+,;=/pchar:@/

 (あー・・ hatena diaryさん、丸カッコのところで自動リンク切っちゃったね。残念!ハズレ〜!)

で、先ほどの議論のように、Percent-encodingはunreservedな文字はする必要がなくて、もしやっちゃったらURL正規化ではもとに戻したものとして評価すべきということなので、上記変態URLと比べると

同じ : http://example.com/unreserved%2d%2E%5F%7e/./percentenc%2527%3A%2F//subdelims!$&'()*+,;=/pchar:@/
違う : http://example.com/unreserved-._~/percentenc%252527%3A%2F/subdelims!$&'()*+,;=/pchar:@/

ってなるわけですな。。大体理解できた気がする! 満足!

クライアント認証をしないOAuth2

OAuthの重要な考え方の中に、クライアント認証というものがある。(OAuthじゃなくてもあるけど)
OAuthはユーザ認証とクライアント認証を組み合わせて認可トークンを発行するプロトコルだという言い方をしても過言でないと思う。実際OAuth2.0の仕様(Draft16)を読んでも、クライアント認証が前提となっているように読める。しかし、一方でこのクライアント認証について説明をしているSection3には、以下の一説がある。

http://tools.ietf.org/html/draft-ietf-oauth-v2-16#section-3 

In addition, the authorization server MAY allow unauthenticated access token requests when the client identity does not matter (e.g. anonymous client) or when the client identity is established via other means. For readability purposes only, this specification is written under the assumption that the authorization server requires some form of client authentication. However, such language does not affect the authorization server's discretion in allowing unauthenticated client requests.

加えて、クライアントのIdentityがどうでもいいとき(たとえば匿名クライアントなど)や、クライアントのIdentityが他の方法で確立できるときは、認可サーバは未認証のアクセストークン要求を許可することもできる(MAY)。読みやすさだけを目的として、本仕様は、認可サーバが何等かのクライアント認証を必要とする仮定で記述している。しかし、そのような記述があるからといって未認証のクライアントリクエストを許可するというサーバ側判断の自由を妨げるものではない。

即ち、クライアント認証をしないOAuth2という形態があり得るということだし、さらに大事なことには、仕様書上 client_id: REQUIRED なんて書かれていても、クライアント認証しない実装をしたい人はキニスンナ(必須じゃなくてもいい)って言っているようにも読めるということだ。

OAuth2でのクライアント認証

そもそもクライアント認証についての記述はどうなっているか。Section3の冒頭から読んでみる。

Client credentials are used to identify and authenticate the client. The client credentials include a client identifier - a unique string issued to the client to identify itself to the authorization server. The client identifier is not a secret, it is exposed to the resource owner, and MUST NOT be used alone for client authentication. Client authentication is accomplished via additional means such as a matching client password.

クライアント資格情報(credentials)は、クライアントを識別特定(identify)し認証(authenticate)するために使われるものです。クライアント資格情報は、クライアント識別子(identifier)を含みます。クライアント識別子(identifier)は認可サーバに対してクラアントが自分が何者であるかを示すために発行される一意性をもった文字列です。クライアント識別子は秘密ではなく、リソース所有者に開示されるものであり、それ単体でクライアント認証に使ってはいけません(MUST NOT)。クライアント認証は、たとえば対応するクライアントパスワードなどの追加的手段をもってはじめて達成されます。

identification と authenticationは違う概念ですが、ちゃんとそこを使い分けて説明しています。

The methods through which the client obtains its client credentials are beyond the scope of this specification. However, the client registration process typically includes gathering relevant information which is used to educate the resource owner about the client when requesting authorization.

クライアントが自分のクライアント資格情報を取得するための方式は本仕様の範囲外です。しかし典型的なケースとしては、クライアント登録プロセスの中で、認可要求時にリソース所有者にクライアントについて教えるために使われるクライアント関連情報を集めることとなります。

なるほど、クライアント資格情報(id/pwみたいなの)をどう発行するかは、実装で決めてくれということか。
しかし確かに「クライアント登録プロセス」でクライアントの関連情報を集めつつ、そこで見返りにid/pwを発行するというのがふつうの実装になるのだろう。

Due to the nature of some clients, the authorization server should not make assumptions about the confidentiality of client credentials without establishing trust with the client. The authorization server SHOULD NOT issue client credentials to clients incapable of keeping their credentials confidential (typically determined during the client registration process).

一部のクライアントの特性を考えれば、認可サーバはクライアントとの信頼を確立することなしに、クライアント資格情報の機密性が保たれるという前提を置くべきではありません。認可サーバは、自分の資格情報の機密に保つ能力のないクライアントに対しては、クライアント資格情報を発行すべきではありません。(典型的な実装としては、これはクライアント登録プロセスの中で決定します)

  • まあ、クライアント資格情報を発行しても、クライアントがそれを秘密にしておけなかったら、なりすまされちゃうもんね。
  • これは「キャッシュカードに暗証番号をマジックで書いちゃうような人にはキャッシュカードを発行すべきじゃない」みたいな話で、実際のところはなんともならんこともあるだろうなぁ。(実際、銀行はそんなコントロールはしていないはず。注意喚起はしてるけど)

そして、最後に先ほどの「クライアント認証なしでもいいよ」という文章が入ってSection 3の導入部分はおしまい。

3.1 クライアントパスワード認証 (Client Password Authentication)

冒頭に思いっきり。

Pending Consensus

まだ合意形成できてないのね・・

で、基本はBasic認証でクライアントIDとクライアントパスワードを送り付けろと書いてある。
確かにHTTPSでID/PW認証するならBasic認証が一番シンプルだ。

3.2 その他クライアント認証方式 (Other Client Authentication Methods)

あんまりおもしろいことは書いていない。

Basic認証やDigest認証の置き換えとしてのOAuth2

さて本題に戻って、「クライアント認証しないOAuth2」ということを考えてみる。クライアント認証しない場合、ユーザ認証しかしないこととなる。認可判定基準が2要素ではなく1要素であるという意味で考えると、マクロに見ればBasic認証やDigest認証と同じわけで、本質を比較してみるのも面白いかもしれない。(二要素認証とは違うので、二要素認可とでも呼べばいいのだろうか・・)今までBasic認証でやってたところを素直にOAuth2に置き換えるケースを考えてみる。

Basic認証の置き換えとしてのOAuth2というと、「4.3. Resource Owner Password Credentials」にそのような記述がありこれを使う方式が有力だ。

The resource owner password credentials grant type is suitable in cases where the resource owner has a trust relationship with the client, such as its computer operating system or a highly privileged application. The authorization server should take special care when enabling the grant type, and only when other flows are not viable.

The grant type is suitable for clients capable of obtaining the resource owner credentials (username and password, typically using an interactive form). It is also used to migrate existing clients using direct authentication schemes such as HTTP Basic or Digest authentication to OAuth by converting the stored credentials with an access token.

一方で、一要素という意味では4.4のClient Credentialも捨てがたい。

4.3のリソース所有者のPassword Credential はフォームで受け付けるのに対して、4.4のClient Credentials はBasic認証で受け付ける仕様である。4.3については1要素判断となるケースはまれなケース(認可サーバが匿名クライアントを許可する場合など)に限られる。一方で4.4のClient Credentials のケースでは認可は1要素判断固定だ。その意味で、こうなるのは理解可能だ。

Basic認証を振り返る

Basic認証を使う場面を考えると、ふつうはID/PWに実際にエンドユーザのID/PWをいれる使い方をするものだが、開発者はクライアントプログラムを識別・認証するためのID/PWを入れるような「応用」も普通にみんなやってる。すなわちBasic認証はクライアント認証もユーザ認証にも使うことができていたということとなる。しかし確かにBasic認証ではクライアント認証とユーザ認証の2要素を両方同時にやりたいと思ったら、ちょっとつらかった。

HTTPがブラウザからのアクセスしか考えなくてよかった時代にはクライアント認証なんてやる必要はなかったから、そういう仕様でBasic認証は出来上がったのだろう。そして、それをプログラムからのアクセスに応用するときはadhoc的にプログラムをユーザと見立ててなんとかまわっていた。しかし、ブラウザ以外の多彩なクライアントからHTTPでWeb APIが叩かれる世界になると、Web API提供者は2つの認証情報すなわちユーザとクライアントの認証に応じて認可判断をしたくなり、一要素認証文化であるBasic/Digest/WSSEでは足らなくなっちゃった。OAuthの成り立ちってそういうところにある。

クライアント認証を必要としないOAuthって、そういう風に考えると何の意味があるのかよくわからないようにも見える。が、OAuthはクライアントとユーザの2つの認証をするためだけのものではなく、分散アーキテクチャで認証・認可サーバとAPIのサーバが分離しているときにどのようにサーバ感で認可状態を持ちまわるかという課題を解決している側面もあるので、決して無意味なものではない。

さて、気になるのは、そういう背景を再認識したうえで、Basic認証の置き換えをしようとしたときに、本当に仕様書Draft16でいうところの「4.3. リソース所有者パスワード資格情報」で置き換えるべきかということ。1要素か2要素かということが本質的な違いであれば、

「一要素認可のときは、やっぱりBasic認証でいいんじゃない?」

という素朴な考えが浮かぶ。ユーザ認証であろうと、Client認証だろうと、一要素で認証してトークンを発行するときはやっぱり
Basic認証でID/PW送ってね」であるほうが仕様がシンプルになるんじゃないのかなぁということ。

でもそういう仕様だとBasic認証で送られてきたIDがクライアントのIDなのかリソース所有者のIDなのか認可サーバとしては分からなくなっちゃうといけないので、このような仕様になっているという理解&納得は可能だ。


結論としては、Basic認証を置き換える場合、置き換える前のBasic認証のIDがクライアントのものであったら4.4の世界にマップすべきだし、そうでなくエンドユーザのIDであったら4.3の世界にマップするのかな。ただし、ここでいう4.3の世界にはClient IDが存在しないケースとなるので、ここの冒頭で書いたように、仕様書にはclient_idがREQUIREDって書いてあるけど、そこは実際は送らなくても認可サーバはエラーを返さない実装にするべきなのだと思う。すなわちREQUIREDって書いてあるからしょうがないからダミーのclient_idを送るとか、そういう必要はないのではないかということ。
(要確認)