Christian Scholz avatar Christian Scholz committed a5fc9e8

initial import from svn (google)

Comments (0)

Files changed (30)

+syntax:glob
+*.pyo
+*.pyc
+*.egg-info
+*.cache
+*.orig
+*.rej
+*~
+*.o
+.svn
+.build
+include docs/*.txt
+include docs/*.rst
+Introduction
+============
+
+``repoze.who.plugins.openid`` is a plugin for the `repoze.who framework <http://static.repoze.org/whodocs/>`_
+enabling `OpenID <http://openid.net>`_ logins.
+
+For more information read the `documentation <http://quantumcore.org/docs/repoze.who.plugins.openid>`_ 
+or check out the `source code <http://code.google.com/p/repozeopenid/>`_.
+
+
+- fix the SQLStore (make the connstring a connection, add tests)
+- add tests for mem, file store
+- fix the session problem, use cookie fallback if no session is given
+- add SREG/AX/PAPE support
+- make more configuration optional and provide default values
+- rename sql_* to store_sql_* config options
+- is came_from actually working?
+
+- document logging
+- document which errors can occur in self.error_field
Add a comment to this file

docs/.static/quantumcore-small.png

Added
New image

docs/.static/quantumstyle.css

+/**
+ * Sphinx Doc Design
+ */
+
+body {
+    font-family: sans-serif;
+    font-size: 100%;
+    background-color: #202020;
+    color: #000;
+    margin: 0;
+    padding: 0;
+}
+
+/* :::: LAYOUT :::: */
+
+#logo {
+    background-color: #000000;
+}
+
+.homelink {
+	font-weight: bold;
+	font-size: 120%;
+}
+
+div.document {
+}
+
+div.documentwrapper {
+    background-color: #000000;
+    float: left;
+    width: 100%;
+}
+
+div.bodywrapper {
+    margin: 0 0 0 230px;
+}
+
+div.body {
+    background-color: #f8f8f8;
+    padding: 0 20px 30px 20px;
+}
+
+div.sphinxsidebarwrapper {
+    padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+    float: left;
+    width: 230px;
+    margin-left: -100%;
+    font-size: 90%;
+}
+
+div.clearer {
+    clear: both;
+}
+
+div.footer {
+    color: #fff;
+    width: 100%;
+    padding: 9px 0 9px 0;
+    text-align: center;
+    font-size: 75%;
+}
+
+div.footer a {
+    color: #fff;
+    text-decoration: underline;
+}
+
+div.related {
+    background-color: #303030;
+    color: #fff;
+    width: 100%;
+    height: 30px;
+    line-height: 30px;
+    font-size: 90%;
+}
+
+div.related h3 {
+    display: none;
+}
+
+div.related ul {
+    margin: 0;
+    padding: 0 0 0 10px;
+    list-style: none;
+}
+
+div.related li {
+    display: inline;
+}
+
+div.related li.right {
+    float: right;
+    margin-right: 5px;
+}
+
+div.related a {
+    color: white;
+}
+
+/* ::: TOC :::: */
+div.sphinxsidebar h3 {
+    font-family: 'Trebuchet MS', sans-serif;
+    color: white;
+    font-size: 1.4em;
+    font-weight: normal;
+    margin: 0;
+    padding: 0;
+}
+
+div.sphinxsidebar h4 {
+    font-family: 'Trebuchet MS', sans-serif;
+    color: white;
+    font-size: 1.3em;
+    font-weight: normal;
+    margin: 5px 0 0 0;
+    padding: 0;
+}
+
+div.sphinxsidebar p {
+    color: white;
+}
+
+div.sphinxsidebar p.topless {
+    margin: 5px 10px 10px 10px;
+}
+
+div.sphinxsidebar ul {
+    margin: 10px;
+    padding: 0;
+    list-style: none;
+    color: white;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+    margin-left: 20px;
+    list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+div.sphinxsidebar a {
+    color: #98dbcc;
+}
+
+div.sphinxsidebar form {
+    margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+    border: 1px solid #98dbcc;
+    font-family: sans-serif;
+    font-size: 1em;
+}
+
+/* :::: MODULE CLOUD :::: */
+div.modulecloud {
+    margin: -5px 10px 5px 10px;
+    padding: 10px;
+    line-height: 160%;
+    border: 1px solid #cbe7e5;
+    background-color: #f2fbfd;
+}
+
+div.modulecloud a {
+    padding: 0 5px 0 5px;
+}
+
+/* :::: SEARCH :::: */
+ul.search {
+    margin: 10px 0 0 20px;
+    padding: 0;
+}
+
+ul.search li {
+    padding: 5px 0 5px 20px;
+    background-image: url(file.png);
+    background-repeat: no-repeat;
+    background-position: 0 7px;
+}
+
+ul.search li a {
+    font-weight: bold;
+}
+
+ul.search li div.context {
+    color: #888;
+    margin: 2px 0 0 30px;
+    text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+    font-weight: bold;
+}
+
+/* :::: COMMON FORM STYLES :::: */
+
+div.actions {
+    padding: 5px 10px 5px 10px;
+    border-top: 1px solid #cbe7e5;
+    border-bottom: 1px solid #cbe7e5;
+    background-color: #e0f6f4;
+}
+
+form dl {
+    color: #333;
+}
+
+form dt {
+    clear: both;
+    float: left;
+    min-width: 110px;
+    margin-right: 10px;
+    padding-top: 2px;
+}
+
+input#homepage {
+    display: none;
+}
+
+div.error {
+    margin: 5px 20px 0 0;
+    padding: 5px;
+    border: 1px solid #d00;
+    font-weight: bold;
+}
+
+/* :::: INLINE COMMENTS :::: */
+
+div.inlinecomments {
+    position: absolute;
+    right: 20px;
+}
+
+div.inlinecomments a.bubble {
+    display: block;
+    float: right;
+    background-image: url(style/comment.png);
+    background-repeat: no-repeat;
+    width: 25px;
+    height: 25px;
+    text-align: center;
+    padding-top: 3px;
+    font-size: 0.9em;
+    line-height: 14px;
+    font-weight: bold;
+    color: black;
+}
+
+div.inlinecomments a.bubble span {
+    display: none;
+}
+
+div.inlinecomments a.emptybubble {
+    background-image: url(style/nocomment.png);
+}
+
+div.inlinecomments a.bubble:hover {
+    background-image: url(style/hovercomment.png);
+    text-decoration: none;
+    color: #3ca0a4;
+}
+
+div.inlinecomments div.comments {
+    float: right;
+    margin: 25px 5px 0 0;
+    max-width: 50em;
+    min-width: 30em;
+    border: 1px solid #2eabb0;
+    background-color: #f2fbfd;
+    z-index: 150;
+}
+
+div#comments {
+    border: 1px solid #2eabb0;
+    margin-top: 20px;
+}
+
+div#comments div.nocomments {
+    padding: 10px;
+    font-weight: bold;
+}
+
+div.inlinecomments div.comments h3,
+div#comments h3 {
+    margin: 0;
+    padding: 0;
+    background-color: #2eabb0;
+    color: white;
+    border: none;
+    padding: 3px;
+}
+
+div.inlinecomments div.comments div.actions {
+    padding: 4px;
+    margin: 0;
+    border-top: none;
+}
+
+div#comments div.comment {
+    margin: 10px;
+    border: 1px solid #2eabb0;
+}
+
+div.inlinecomments div.comment h4,
+div.commentwindow div.comment h4,
+div#comments div.comment h4 {
+    margin: 10px 0 0 0;
+    background-color: #2eabb0;
+    color: white;
+    border: none;
+    padding: 1px 4px 1px 4px;
+}
+
+div#comments div.comment h4 {
+    margin: 0;
+}
+
+div#comments div.comment h4 a {
+    color: #d5f4f4;
+}
+
+div.inlinecomments div.comment div.text,
+div.commentwindow div.comment div.text,
+div#comments div.comment div.text {
+    margin: -5px 0 -5px 0;
+    padding: 0 10px 0 10px;
+}
+
+div.inlinecomments div.comment div.meta,
+div.commentwindow div.comment div.meta,
+div#comments div.comment div.meta {
+    text-align: right;
+    padding: 2px 10px 2px 0;
+    font-size: 95%;
+    color: #538893;
+    border-top: 1px solid #cbe7e5;
+    background-color: #e0f6f4;
+}
+
+div.commentwindow {
+    position: absolute;
+    width: 500px;
+    border: 1px solid #cbe7e5;
+    background-color: #f2fbfd;
+    display: none;
+    z-index: 130;
+}
+
+div.commentwindow h3 {
+    margin: 0;
+    background-color: #2eabb0;
+    color: white;
+    border: none;
+    padding: 5px;
+    font-size: 1.5em;
+    cursor: pointer;
+}
+
+div.commentwindow div.actions {
+    margin: 10px -10px 0 -10px;
+    padding: 4px 10px 4px 10px;
+    color: #538893;
+}
+
+div.commentwindow div.actions input {
+    border: 1px solid #2eabb0;
+    background-color: white;
+    color: #135355;
+    cursor: pointer;
+}
+
+div.commentwindow div.form {
+    padding: 0 10px 0 10px;
+}
+
+div.commentwindow div.form input,
+div.commentwindow div.form textarea {
+    border: 1px solid #3c9ea2;
+    background-color: white;
+    color: black;
+}
+
+div.commentwindow div.error {
+    margin: 10px 5px 10px 5px;
+    background-color: #fbe5dc;
+    display: none;
+}
+
+div.commentwindow div.form textarea {
+    width: 99%;
+}
+
+div.commentwindow div.preview {
+    margin: 10px 0 10px 0;
+    background-color: #70d0d4;
+    padding: 0 1px 1px 25px;
+}
+
+div.commentwindow div.preview h4 {
+    margin: 0 0 -5px -20px;
+    padding: 4px 0 0 4px;
+    color: white;
+    font-size: 1.3em;
+}
+
+div.commentwindow div.preview div.comment {
+    background-color: #f2fbfd;
+}
+
+div.commentwindow div.preview div.comment h4 {
+    margin: 10px 0 0 0!important;
+    padding: 1px 4px 1px 4px!important;
+    font-size: 1.2em;
+}
+
+/* :::: SUGGEST CHANGES :::: */
+div#suggest-changes-box input, div#suggest-changes-box textarea {
+    border: 1px solid #ccc;
+    background-color: white;
+    color: black;
+}
+
+div#suggest-changes-box textarea {
+    width: 99%;
+    height: 400px;
+}
+
+
+/* :::: PREVIEW :::: */
+div.preview {
+    background-image: url(style/preview.png);
+    padding: 0 20px 20px 20px;
+    margin-bottom: 30px;
+}
+
+
+/* :::: INDEX PAGE :::: */
+
+table.contentstable {
+    width: 90%;
+}
+
+table.contentstable p.biglink {
+    line-height: 150%;
+}
+
+a.biglink {
+    font-size: 1.3em;
+}
+
+span.linkdescr {
+    font-style: italic;
+    padding-top: 5px;
+    font-size: 90%;
+}
+
+/* :::: INDEX STYLES :::: */
+
+table.indextable td {
+    text-align: left;
+    vertical-align: top;
+}
+
+table.indextable dl, table.indextable dd {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+table.indextable tr.pcap {
+    height: 10px;
+}
+
+table.indextable tr.cap {
+    margin-top: 10px;
+    background-color: #f2f2f2;
+}
+
+img.toggler {
+    margin-right: 3px;
+    margin-top: 3px;
+    cursor: pointer;
+}
+
+form.pfform {
+    margin: 10px 0 20px 0;
+}
+
+/* :::: GLOBAL STYLES :::: */
+
+.docwarning {
+    background-color: #ffe4e4;
+    padding: 10px;
+    margin: 0 -20px 0 -20px;
+    border-bottom: 1px solid #f66;
+}
+
+p.subhead {
+    font-weight: bold;
+    margin-top: 20px;
+}
+
+a {
+    color: #355f7c;
+    text-decoration: none;
+}
+
+a:hover {
+    text-decoration: underline;
+}
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+    font-family: 'Trebuchet MS', sans-serif;
+    background-color: #f2f2f2;
+    font-weight: normal;
+    color: #20435c;
+    border-bottom: 1px solid #ccc;
+    margin: 20px -20px 10px -20px;
+    padding: 3px 0 3px 10px;
+}
+
+div.body h1 { margin-top: 0; font-size: 200%; }
+div.body h2 { font-size: 160%; }
+div.body h3 { font-size: 140%; }
+div.body h4 { font-size: 120%; }
+div.body h5 { font-size: 110%; }
+div.body h6 { font-size: 100%; }
+
+a.headerlink {
+    color: #c60f0f;
+    font-size: 0.8em;
+    padding: 0 4px 0 4px;
+    text-decoration: none;
+    visibility: hidden;
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink {
+    visibility: visible;
+}
+
+a.headerlink:hover {
+    background-color: #c60f0f;
+    color: white;
+}
+
+div.body p, div.body dd, div.body li {
+    text-align: justify;
+    line-height: 130%;
+}
+
+div.body p.caption {
+    text-align: inherit;
+}
+
+div.body td {
+    text-align: left;
+}
+
+ul.fakelist {
+    list-style: none;
+    margin: 10px 0 10px 20px;
+    padding: 0;
+}
+
+.field-list ul {
+    padding-left: 1em;
+}
+
+.first {
+    margin-top: 0 !important;
+}
+
+/* "Footnotes" heading */
+p.rubric {
+    margin-top: 30px;
+    font-weight: bold;
+}
+
+/* "Topics" */
+
+div.topic {
+    background-color: #eee;
+    border: 1px solid #ccc;
+    padding: 0 7px 0 7px;
+    margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+    font-size: 1.1em;
+    font-weight: bold;
+    margin-top: 10px;
+}
+
+/* Admonitions */
+
+div.admonition {
+    margin-top: 10px;
+    margin-bottom: 10px;
+    padding: 7px;
+}
+
+div.admonition dt {
+    font-weight: bold;
+}
+
+div.admonition dl {
+    margin-bottom: 0;
+}
+
+div.admonition p {
+    display: inline;
+}
+
+div.seealso {
+    background-color: #ffc;
+    border: 1px solid #ff6;
+}
+
+div.warning {
+    background-color: #ffe4e4;
+    border: 1px solid #f66;
+}
+
+div.note {
+    background-color: #eee;
+    border: 1px solid #ccc;
+}
+
+p.admonition-title {
+    margin: 0px 10px 5px 0px;
+    font-weight: bold;
+    display: inline;
+}
+
+p.admonition-title:after {
+    content: ":";
+}
+
+div.body p.centered {
+    text-align: center;
+    margin-top: 25px;
+}
+
+table.docutils {
+    border: 0;
+}
+
+table.docutils td, table.docutils th {
+    padding: 1px 8px 1px 0;
+    border-top: 0;
+    border-left: 0;
+    border-right: 0;
+    border-bottom: 1px solid #aaa;
+}
+
+table.field-list td, table.field-list th {
+    border: 0 !important;
+}
+
+table.footnote td, table.footnote th {
+    border: 0 !important;
+}
+
+.field-list ul {
+    margin: 0;
+    padding-left: 1em;
+}
+
+.field-list p {
+    margin: 0;
+}
+
+dl {
+    margin-bottom: 15px;
+    clear: both;
+}
+
+dd p {
+    margin-top: 0px;
+}
+
+dd ul, dd table {
+    margin-bottom: 10px;
+}
+
+dd {
+    margin-top: 3px;
+    margin-bottom: 10px;
+    margin-left: 30px;
+}
+
+.refcount {
+    color: #060;
+}
+
+dt:target,
+.highlight {
+    background-color: #fbe54e;
+}
+
+dl.glossary dt {
+    font-weight: bold;
+    font-size: 1.1em;
+}
+
+th {
+    text-align: left;
+    padding-right: 5px;
+}
+
+pre {
+    padding: 5px;
+    background-color: #efc;
+    color: #333;
+    border: 1px solid #ac9;
+    border-left: none;
+    border-right: none;
+    overflow: auto;
+}
+
+td.linenos pre {
+    padding: 5px 0px;
+    border: 0;
+    background-color: transparent;
+    color: #aaa;
+}
+
+table.highlighttable {
+    margin-left: 0.5em;
+}
+
+table.highlighttable td {
+    padding: 0 0.5em 0 0.5em;
+}
+
+tt {
+    background-color: #ecf0f3;
+    padding: 0 1px 0 1px;
+    font-size: 0.95em;
+}
+
+tt.descname {
+    background-color: transparent;
+    font-weight: bold;
+    font-size: 1.2em;
+}
+
+tt.descclassname {
+    background-color: transparent;
+}
+
+tt.xref, a tt {
+    background-color: transparent;
+    font-weight: bold;
+}
+
+.footnote:target  { background-color: #ffa }
+
+h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
+    background-color: transparent;
+}
+
+.optional {
+    font-size: 1.3em;
+}
+
+.versionmodified {
+    font-style: italic;
+}
+
+form.comment {
+    margin: 0;
+    padding: 10px 30px 10px 30px;
+    background-color: #eee;
+}
+
+form.comment h3 {
+    background-color: #326591;
+    color: white;
+    margin: -10px -30px 10px -30px;
+    padding: 5px;
+    font-size: 1.4em;
+}
+
+form.comment input,
+form.comment textarea {
+    border: 1px solid #ccc;
+    padding: 2px;
+    font-family: sans-serif;
+    font-size: 100%;
+}
+
+form.comment input[type="text"] {
+    width: 240px;
+}
+
+form.comment textarea {
+    width: 100%;
+    height: 200px;
+    margin-bottom: 10px;
+}
+
+.system-message {
+    background-color: #fda;
+    padding: 5px;
+    border: 3px solid red;
+}
+
+/* :::: PRINT :::: */
+@media print {
+    div.document,
+    div.documentwrapper,
+    div.bodywrapper {
+        margin: 0;
+        width : 100%;
+    }
+
+    div.sphinxsidebar,
+    div.related,
+    div.footer,
+    div#comments div.new-comment-box,
+    #top-link {
+        display: none;
+    }
+}

docs/.templates/layout.html

+{# Filename: .templates/layout.html #}
+{% extends '!layout.html' %}
+
+{% block relbar1 %}
+<div id="logo">
+<a href="/"><img src="{{ pathto('_static/quantumcore-small.png',1) }}"/></a>
+</div>
+    <div class="related">
+      <h3>Navigation</h3>
+      <ul>
+        {%- for rellink in rellinks %}
+        <li class="right" {% if loop.first %}style="margin-right: 10px"{% endif %}>
+          <a href="{{ pathto(rellink[0]) }}" title="{{ rellink[1]|striptags }}"
+             accesskey="{{ rellink[2] }}">{{ rellink[3] }}</a>
+          {%- if not loop.first %}{{ reldelim2 }}{% endif %}</li>
+        {%- endfor %}
+        {%- block rootrellink %}
+        <li><a class="homelink" href="{{ pathto('index') }}">{{ shorttitle }}</a>{{ reldelim1 }}</li>
+        {%- endblock %}
+        {%- for parent in parents %}
+          <li><a href="{{ parent.link|e }}" accesskey="U">{{ parent.title }}</a>{{ reldelim1 }}</li>
+        {%- endfor %}
+        {%- block relbaritems %}{% endblock %}
+      </ul>
+    </div>
+
+{% endblock %}
+
+Changelog
+=========
+
+
+0.5.3 - TBD
+-----------
+
+* added webob to requirements in setup.py
+
+
+
+0.5.2 - September 25th 2009
+---------------------------
+
+* added MANIFEST.in to make egg work again 
+
+
+
+0.5.1 - September 25th 2009
+---------------------------
+
+* fixed setup.py (removed doubles test suite)
+
+
+
+0.5 - March 15th 2009
+---------------------
+
+* Initial release
+
+
+
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build -a
+PAPER         =
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html      to make standalone HTML files"
+	@echo "  pickle    to make pickle files (usable by e.g. sphinx-web)"
+	@echo "  htmlhelp  to make HTML files and a HTML help project"
+	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  changes   to make an overview over all changed/added/deprecated items"
+	@echo "  linkcheck to check all external links for integrity"
+
+clean:
+	-rm -rf .build/*
+
+html:
+	mkdir -p .build/html .build/doctrees
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html
+	@echo
+	@echo "Build finished. The HTML pages are in .build/html."
+
+pickle:
+	mkdir -p .build/pickle .build/doctrees
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files or run"
+	@echo "  sphinx-web .build/pickle"
+	@echo "to start the sphinx-web server."
+
+web: pickle
+
+htmlhelp:
+	mkdir -p .build/htmlhelp .build/doctrees
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in .build/htmlhelp."
+
+latex:
+	mkdir -p .build/latex .build/doctrees
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in .build/latex."
+	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+	      "run these through (pdf)latex."
+
+changes:
+	mkdir -p .build/changes .build/doctrees
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes
+	@echo
+	@echo "The overview file is in .build/changes."
+
+linkcheck:
+	mkdir -p .build/linkcheck .build/doctrees
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in .build/linkcheck/output.txt."

docs/api/identification.rst

+.. _identification_module:
+
+:mod:`repoze.who.plugins.openid.identification`
+-----------------------------------------------
+
+.. automodule:: repoze.who.plugins.openid.identification
+
+  .. autoclass:: OpenIdIdentificationPlugin
+    :members:
+
+

docs/basicflow.rst

+Sequence of an OpenID authentication process
+--------------------------------------------
+
+``repoze.who`` consists of several plugins which work together during the OpenID authentication sequence in the following way:
+
+    1. The user enters a page which needs authentication
+    2. The application raises an "401 Unauthorized" exception
+    3. The ``IChallengeDecider`` plugin decides that the ``IChallenge`` plugin needs to be called
+    4. The ``IChallenge`` plugin checks for the 401 and redirects the user to the URL defined in ``login_form_url``
+    5. The user enters an OpenID into the login form and submits it. The URL given in the configuration option ``login_handler_url`` is POSTed to.
+    6. The ``IIdentification`` plugin detects the URL given and checks if an openid is present in the POST data. If this is the case it copies the openid into the WSGI environment so that it's read later by the ``IChallenge`` plugin (which is called after the application has done it's part, which in this case is probably returning a ``404`` error because you don't need to implement the login handler as it's handled then by the challenge plugin)
+    7. On egress with that ``404`` error the ``IChallengeDecider`` checks this time if an OpenID is present in the WSGI environment. If this is the case it will allow the ``IChallenge`` plugin to run
+    8. The ``IChallenge`` plugin checks if the URL given in ``login_handler_path`` is active and if an OpenID is present in the environment. If this is the case it will start the OpenID discovery process using the Python OpenID library. It will return a WSGI application which will redirect the user to the OpenID provider.
+    9. Coming back from the OpenID provider the user calls the URL given in ``login_handler_path`` again because this was the URL the plugin gave to the provider to redirect back to. The ``IIdentification`` plugin is called again on ingress and it checks again the URL to be correct and the result of the OpenID authentication (using the library). If everything was ok it stores the authenticated OpenID in the identity dict as ``repoze.who.plugins.openid.userid``. This is additionally remembered via the plugin given in the configuration option ``rememberer_name`` (usually this is ``auth_tkt``)
+    10. The ``IAuthenticate`` plugin is called next and converts the found openid into a userid which is returned (``None`` means that no authentication took place). The dummy authenticator shipped with this plugin will simply copy the openid over as userid. Usually you should write your own plugin which might do some database lookup to find the correct user.
+
+And this finishes the OpenID process.
+
+# -*- coding: utf-8 -*-
+#
+# repoze.who openid plugin documentation build configuration file, created by
+# sphinx-quickstart on Sat Nov  1 11:43:55 2008.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# The contents of this file are pickled, so don't put values in the namespace
+# that aren't pickleable (module imports are okay, they're removed automatically).
+#
+# All configuration values have a default value; values that are commented out
+# serve to show the default value.
+
+import sys, os
+
+# If your extensions are in another directory, add it here. If the directory
+# is relative to the documentation root, use os.path.abspath to make it
+# absolute, like shown here.
+#sys.path.append(os.path.abspath('some/directory'))
+# 
+parent = os.path.dirname(os.path.dirname(__file__))
+sys.path.append(os.path.abspath(parent))
+wd = os.getcwd()
+os.chdir(parent)
+os.system('%s setup.py develop -qN' % sys.executable)
+os.chdir(wd)
+print sys.executable
+
+print parent
+
+for item in os.listdir(parent):
+    if item.endswith('.egg'):
+        sys.path.append(os.path.join(parent, item))
+
+
+# General configuration
+# ---------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['.templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General substitutions.
+project = 'repoze.who openid plugin'
+copyright = '2008, Christian Scholz'
+
+# The default replacements for |version| and |release|, also used in various
+# other places throughout the built documents.
+#
+# The short X.Y version.
+version = '1.0'
+# The full version, including alpha/beta/rc tags.
+release = '1.0b1'
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directories, that shouldn't be searched
+# for source files.
+#exclude_dirs = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+
+# Options for HTML output
+# -----------------------
+
+# The style sheet to use for HTML and HTML Help pages. A file of that name
+# must exist either in Sphinx' static/ path, or in one of the custom paths
+# given in html_static_path.
+html_style = 'quantumstyle.css'
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (within the static path) to place at the top of
+# the sidebar.
+#html_logo = 'small.png'
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['.static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, the reST sources are included in the HTML build as _sources/<name>.
+#html_copy_source = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'repozewhoopenidplugindoc'
+
+
+# Options for LaTeX output
+# ------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, document class [howto/manual]).
+latex_documents = [
+  ('index', 'repozewhoopenidplugin.tex', 'repoze.who openid plugin Documentation',
+   'Christian Scholz', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
+
+autoclass_content="class"
+.. repoze.who openid plugin documentation master file, created by sphinx-quickstart on Sat Nov  1 11:43:55 2008.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to repoze.who openid plugin's documentation!
+====================================================
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   installation
+   basicflow
+   usage
+   api/identification
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+

docs/installation.rst

+Installation of the plugin
+==========================
+
+To install the plugin all you have to do is to use ``easy_install``::
+
+    easy_install repoze.who.plugins.openid
+
+This will install the plugin with all it's dependencies (like the OpenID library).
+
+If you use buildout you can simply add ``repoze.who.plugins.openid`` to the list
+of Python eggs to install.
+
+
+
+
+Configuration
+=============
+
+.. Module: repoze.who.plugins.openid.identitification
+
+The OpenID plugin is configured like all the other ``repoze.who`` plugins via
+the ``who.ini`` file (or what the name of it happens to be according to your
+main ``.ini`` file.
+
+Here is an example of the openid-plugin-section::
+
+
+          [plugin:openid]
+
+          use = repoze.who.plugins.openid:make_identification_plugin
+
+          store = file 
+          store_file_path = %(here)s/sstore
+          openid_field = openid
+          came_from_field = came_from
+          error_field = error
+          session_name = beaker.session
+          login_form_url = /login_form
+          login_handler_path = /do_login
+          logout_handler_path = /logout
+          logged_in_url = /success
+          logged_out_url = /logout_success
+          rememberer_name = auth_tkt
+
+A more complete example will be given below. 
+
+Configuration options
+---------------------
+
+Here is a list of all possible configuration options and there possible values:
+
+.. describe:: store
+
+    Defines which OpenID store implementation to use. Possible values are
+    ``mem``, ``file`` and ``sql``. Depending on what you choose here you need 
+    to give additional values such as a file path or sql connection string.
+
+    ``mem`` means to use a RAM based store for OpenID associations. No further
+    configuration is possible here.
+
+    ``file`` means to use a file based store for OpenID associations. You need
+    to give the path to the file being used as ``store_file_path`` option.
+
+    ``sql`` means to use an SQL database for storing OpenID associations. You
+    need at least give a connection string as ``store_sql_connstring`` configuration
+    option. Additionally you can choose which tables to use. The defaults are
+    ``oid_associations`` and ``oid_nonces`` but they can be configured by the 
+    configuration options ``store_sql_associations_table`` and ``store_sql_nonces_table``
+    respectively. Check the OpenID library documentation for more info on
+    these tables and how to use the SQL store.
+    
+    .. warning:: The SQL implementation is not working at the moment.
+
+.. describe:: openid_field
+
+    You define here in which field in the request coming from a login form the
+    OpenID of the user is stored. Default is ``openid``.
+    
+.. describe:: came_from_field
+
+    ``came_from_field`` defines in which field in a request coming from the login form the URL is stored to which to redirect after successful authentication. The default is ``came_from``.
+    
+    .. warning:: This is not really tested and might actually not work due to
+                 the redirections of the OpenID process itself. Better use ``logged_in_url`` for this. 
+    
+.. describe:: error_field
+
+    This directive defines in which field in the WSGI environment OpenID errors will be written should they occur. The default is ``error``.
+    
+.. describe:: session_name
+
+    OpenIDs requires a session for the whole login process (basically from sending the user to the OpenID provider to the provider redirecting back and checking the result). The default is to use a cookie internally. 
+    
+    If you are using your own session middleware anyway and it's providing
+    a dictionary interface in an WSGI environment variable then you can 
+    configure this to be used by providing the name of this variable as ``session_name``.
+    
+    Example::
+    
+        session_name = beaker.session
+
+.. describe:: login_form_url
+
+    This directive defines under which path the login page is to be found. This needs to be configured so the challenge plugin can redirect to it.
+    
+    The login page is supposed to ask the user for the OpenID to be used to login which then is supposed to be stored in a field named as configured with ``openid_field``. 
+    
+
+.. describe:: login_handler_path
+
+    This configuration defines the URL the login form POSTs it's data to.
+    You need to define this because the OpenID process will also use this URL
+    to know when an OpenID authentication process is active as the OpenID provider will redirect back to this URL. The plugin will then intercept this
+    redirect and parse the results.
+    
+    You don't really have to write a view for this URL as it's just there to
+    be intercepted by this plugin.
+    
+    In case of a login success the user will be redirected to the URL defined
+    in ``logged_in_url``. In case of an error the login form will be displayed
+    again.
+
+.. describe:: logout_handler_path
+
+    In order to be able to log a user out again you have to give a path 
+    which you send the user to. This URL again does not need to be implemented
+    as a view but only serves as marker for this plugin to know.
+    
+    After the logout has happened the user will be redirect to the URL defined
+    in ``logged_out_url``.
+
+.. describe:: logged_in_url
+
+    Store the URL in here to which the user should be redirected after a 
+    successful login. You need to define this and you need to implement the
+    view for it.
+
+.. describe:: logged_out_url
+
+    Store the URL in here to which the user should be redirected after a 
+    successful logout. You need to define this and you need to implement the
+    view for it.
+
+.. describe:: rememberer_name
+
+    Place the name of the identification plugin here which is used to remember
+    a successful authentication. You can e.g. configure the ``auth_tkt`` 
+    plugin as done in the ``repoze.who`` example and just put this in here::
+    
+        rememberer_name = auth_tkt
+        
+    The result is that the openid of the user will be stored as cookie via the
+    ``auth_tkt`` plugin. This plugin also makes it somewhat sure that the 
+    value is not plain text in the cookie but encrypted at least somewhat.
+    
+    Other possibilities are here to e.g. use a session for this you have
+    anyway. Check the ``repoze.who`` documentation on how to write a
+    rememberer plugin.
+    
+    
+The challenge decider
+---------------------
+
+In order to trigger the OpenID process the challenge plugin needs to know when
+to really start it. Usually this needs to be done if an OpenID is present in the request. Per default the challenger is only called for ``Unauthorized`` responses from the application. In order to also trigger it for requests containing the field defined in ``openid_field`` you also have to configure a different Challengde Decider in your ``who.ini``::
+
+    [general]
+    challenge_decider = repoze.who.plugins.openid.classifiers:openid_challenge_decider
+
+    
+Complete example
+----------------
+    
+In order to show how all the plugins work together here is a complete ``who.ini``:
+
+.. literalinclude:: who.ini
+   :language: none
+
+
+
+[plugin:basicauth]
+# identification and challenge
+use = repoze.who.plugins.basicauth:make_plugin
+realm = 'sample'
+
+[plugin:openid]
+# identification and challenge
+use = repoze.who.plugins.openid:make_identification_plugin
+# sql and file are possible here with different configurations
+store = file 
+store_file_path = %(here)s/sstore
+openid_field = openid
+came_from_field = came_from
+error_field = error
+session_name = beaker.session
+login_form_url = /login_form
+login_handler_path = /do_login
+logout_handler_path = /logout
+logged_in_url = /success
+logged_out_url = /logout_success
+rememberer_name = auth_tkt
+
+
+[plugin:auth_tkt]
+# identification
+use = repoze.who.plugins.auth_tkt:make_plugin
+secret = s33kr1t
+cookie_name = oatmeal
+secure = False
+include_ip = False
+
+[plugin:htpasswd]
+# authentication
+use = repoze.who.plugins.htpasswd:make_plugin
+filename = %(here)s/passwd
+check_fn = repoze.who.plugins.htpasswd:crypt_check
+
+[general]
+request_classifier = repoze.who.classifiers:default_request_classifier
+challenge_decider = repoze.who.plugins.openid.classifiers:openid_challenge_decider
+
+[identifiers]
+plugins =
+      openid
+      auth_tkt
+
+[authenticators]
+# plugin_name;classifier_name.. or just plugin_name (good for any)
+plugins =
+      openid
+
+[challengers]
+# plugin_name;classifier_name:.. or just plugin_name (good for any)
+plugins =
+      openid

repoze/__init__.py

+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)

repoze/who/__init__.py

+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)

repoze/who/plugins/__init__.py

+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)

repoze/who/plugins/openid/__init__.py

+from identification import OpenIdIdentificationPlugin
+
+def make_identification_plugin(store='mem',
+                openid_field = "openid",
+                session_name = None,
+                login_handler_path = None,
+                logout_handler_path = None,
+                login_form_url = None,
+                error_field = 'error',
+                logged_in_url = None,
+                logged_out_url = None,
+                came_from_field = 'came_from',
+                store_file_path='',
+                rememberer_name = None,
+                sql_associations_table = '',
+                sql_nonces_table = '',
+                sql_connstring = ''):
+    if store not in (u'mem',u'file',u'sql'):
+        raise ValueError("store needs to be 'mem', 'sql' or 'file'")
+    if login_form_url is None:
+        raise ValueError("login_form_url needs to be given")
+    if rememberer_name is None:
+        raise ValueError("rememberer_name needs to be given")
+    if login_handler_path is None:
+        raise ValueError("login_handler_path needs to be given")
+    if logout_handler_path is None:
+        raise ValueError("logout_handler_path needs to be given")
+    if session_name is None:
+        raise ValueError("session_name needs to be given")
+    if logged_in_url is None:
+        raise ValueError("logged_in_url needs to be given")
+    if logged_out_url is None:
+        raise ValueError("logged_out_url needs to be given")
+
+    plugin = OpenIdIdentificationPlugin(store, 
+        openid_field = openid_field,
+        error_field = error_field,
+        session_name = session_name,
+        login_form_url = login_form_url,
+        login_handler_path = login_handler_path,
+        logout_handler_path = logout_handler_path,
+        store_file_path = store_file_path,
+        logged_in_url = logged_in_url,
+        logged_out_url = logged_out_url,
+        came_from_field = came_from_field,
+        rememberer_name = rememberer_name,
+        sql_associations_table = sql_associations_table,
+        sql_nonces_table = sql_nonces_table,
+        sql_connstring = sql_connstring
+        )
+    return plugin
+

repoze/who/plugins/openid/classifiers.py

+import zope.interface 
+from repoze.who.interfaces import IChallengeDecider
+
+
+def openid_challenge_decider(environ, status, headers):
+    # we do the default if it's a 401, probably we show a form then
+    if status.startswith('401 '):
+        return True
+    elif environ.has_key('repoze.whoplugins.openid.openid'):
+        # in case IIdentification found an openid it should be in the environ
+        # and we do the challenge
+        return True
+    return False
+    
+zope.interface.directlyProvides(openid_challenge_decider, IChallengeDecider)
+

repoze/who/plugins/openid/id2

+import cgi
+import urlparse
+import cgitb
+import sys
+from zope.interface import implements
+
+from repoze.who.interfaces import IChallenger
+from repoze.who.interfaces import IIdentifier
+from repoze.who.interfaces import IAuthenticator
+
+from webob import Request, Response
+
+import openid
+from openid.store import memstore, filestore, sqlstore
+from openid.consumer import consumer
+from openid.oidutil import appendArgs
+from openid.cryptutil import randomString
+from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
+from openid.extensions import pape, sreg
+
+
+
+class OpenIdIdentificationPlugin(object):
+    """The repoze.who OpenID plugin
+    
+    This class contains 3 plugin types and is thus implementing
+    IIdentifier, IChallenger and IAuthenticator.
+    (check the `repoze.who documentation <http://static.repoze.org/bfgdocs/>`_
+    for what all these plugin types do.)
+    
+    """
+
+
+    implements(IChallenger, IIdentifier, IAuthenticator)
+
+    def __init__(self, store, 
+                    openid_field, 
+                    error_field = '',
+                    store_file_path='',
+                    session_name = '',
+                    login_handler_path = '',
+                    logout_handler_path = '',
+                    login_form_url = '',
+                    logged_in_url = '',
+                    logged_out_url = '',
+                    came_from_field = '',
+                    rememberer_name = '',
+                    sql_associations_table = '',
+                    sql_nonces_table = '',
+                    sql_connstring = '',
+                    hans=10,
+                    huns=20):
+
+        self.rememberer_name = rememberer_name
+        self.login_handler_path = login_handler_path
+        self.logout_handler_path = logout_handler_path
+        self.login_form_url = login_form_url
+        self.session_name = session_name
+        self.error_field = error_field
+        self.came_from_field = came_from_field
+        self.logged_out_url = logged_out_url
+        self.logged_in_url = logged_in_url
+        
+        # for the SQL store
+        self.sql_associations_table = sql_associations_table
+        self.sql_nonces_table = sql_nonces_table
+        self.sql_connstring = sql_connstring
+        
+        # set up the store
+        if store==u"file":
+            self.store = filestore.FileOpenIDStore(store_file_path)
+        elif store==u"mem":
+            self.store = memstore.MemoryStore()
+        elif store==u"sql":
+            # TODO: This does not work as we need a connection, not a string
+            self.store = sqlstore.SQLStore(sql_connstring, sql_associations_table, sql_connstring)
+        self.openid_field = openid_field
+            
+    def _get_rememberer(self, environ):
+        rememberer = environ['repoze.who.plugins'][self.rememberer_name]
+        return rememberer
+
+    def get_consumer(self,environ):
+        session = environ.get(self.session_name,{})
+        return consumer.Consumer(session,self.store)
+        
+    def redirect_to_logged_in(self, environ):
+        """redirect to came_from or standard page if login was successful"""
+        request = Request(environ)
+        came_from = request.params.get(self.came_from_field,'')
+        if came_from!='':
+            url = came_from
+        else:
+            url = self.logged_in_url
+        res = Response()
+        res.status = 302
+        res.location = url
+        environ['repoze.who.application'] = res    
+
+    # IIdentifier
+    def identify(self, environ):
+        """this method is called when a request is incoming.
+
+        After the challenge has been called we might get here a response
+        from an openid provider.
+
+        """
+
+        request = Request(environ)
+        
+        # first test for logout as we then don't need the rest
+        if request.path == self.logout_handler_path:
+            res = Response()
+            # set forget headers
+            for a,v in self.forget(environ,{}):
+                res.headers.add(a,v)
+            res.status = 302
+            res.location = self.logged_out_url
+            environ['repoze.who.application'] = res
+            return {}
+
+        identity = {}
+
+        # first we check we are actually on the URL which is supposed to be the
+        # url to return to (login_handler_path in configuration)
+        # this URL is used for both: the answer for the login form and
+        # when the openid provider redirects the user back.
+        if request.path == self.login_handler_path:
+
+        # in the case we are coming from the login form we should have 
+        # an openid in here the user entered
+            open_id = request.params.get(self.openid_field, None)
+            environ['repoze.who.logger'].debug('checking openid results for : %s ' %open_id)
+            
+            if open_id is not None:
+                open_id = open_id.strip()
+            
+            # we don't do anything with the openid we found ourselves but we put it in here
+            # to tell the challenge plugin to initiate the challenge
+            identity['repoze.whoplugins.openid.openid'] = environ['repoze.whoplugins.openid.openid'] = open_id
+        
+            # this part now is for the case when the openid provider redirects
+            # the user back. We should find some openid specific fields in the request.
+            mode=request.params.get("openid.mode", None)
+            if mode=="id_res":
+                oidconsumer = self.get_consumer(environ)
+                info = oidconsumer.complete(request.params, request.url)
+
+                if info.status == consumer.SUCCESS:
+                    environ['repoze.who.logger'].info('openid request successful for : %s ' %open_id)
+                    
+                    display_identifier = info.identity_url
+                    
+                    # remove this so that the challenger is not triggered again
+                    del environ['repoze.whoplugins.openid.openid']
+                    
+                    # store the id for the authenticator
+                    identity['repoze.who.plugins.openid.userid'] = display_identifier
+
+                    # now redirect to came_from or the success page
+                    self.redirect_to_logged_in(environ)
+                    return identity
+                    
+                # TODO: Do we have to check for more failures and such?
+                # 
+            elif mode=="cancel":
+                # cancel is a negative assertion in the OpenID protocol,
+                # which means the user did not authorize correctly.
+                environ['repoze.whoplugins.openid.error'] = 'OpenID authentication failed.'
+                pass
+        return identity
+
+    # IIdentifier
+    def remember(self, environ, identity):
+        """remember the openid in the session we have anyway"""
+        rememberer = self._get_rememberer(environ)
+        r = rememberer.remember(environ, identity)
+        return r
+
+    # IIdentifier
+    def forget(self, environ, identity):
+        """forget about the authentication again"""
+        rememberer = self._get_rememberer(environ)
+        return rememberer.forget(environ, identity)
+
+    # IChallenge
+    def challenge(self, environ, status, app_headers, forget_headers):
+        """the challenge method is called when the ``IChallengeDecider``
+        in ``classifiers.py`` returns ``True``. This is the case for either a 
+        ``401`` response from the client or if the key 
+        ``repoze.whoplugins.openid.openidrepoze.whoplugins.openid.openid``
+        is present in the WSGI environment.
+        The name of this key can be adjusted via the ``openid_field`` configuration
+        directive.
+
+        The latter is the case when we are coming from the login page where the
+        user entered the openid to use.
+
+        ``401`` can come back in any case and then we simply redirect to the login
+        form which is configured in the who configuration as ``login_form_url``.
+
+        TODO: make the environment key to check also configurable in the challenge_decider.
+
+        For the OpenID flow check `the OpenID library documentation 
+        <http://openidenabled.com/files/python-openid/docs/2.2.1/openid.consumer.consumer-module.html>`_
+
+        """
+
+        request = Request(environ)
+        
+        # check for the field present, if not redirect to login_form
+        if not request.params.has_key(self.openid_field):
+            # redirect to login_form
+            res = Response()
+            res.status = 302
+            res.location = self.login_form_url+"?%s=%s" %(self.came_from_field, request.url)
+            return res
+
+        # now we have an openid from the user in the request 
+        openid_url = request.params[self.openid_field]
+        environ['repoze.who.logger'].debug('starting openid request for : %s ' %openid_url)       
+
+        try:
+        # we create a new Consumer and start the discovery process for the URL given
+        # in the library openid_request is called auth_req btw.
+            openid_request = self.get_consumer(environ).begin(openid_url)
+        except consumer.DiscoveryFailure, exc:
+            # eventually no openid server could be found
+            environ[self.error_field] = 'Error in discovery: %s' %exc[0]
+            environ['repoze.who.logger'].info('Error in discovery: %s ' %exc[0])     
+            return self._redirect_to_loginform(environ)
+        except KeyError, exc:
+            # TODO: when does that happen, why does plone.openid use "pass" here?
+            environ[self.error_field] = 'Error in discovery: %s' %exc[0]
+            environ['repoze.who.logger'].info('Error in discovery: %s ' %exc[0])
+            return self._redirect_to_loginform(environ)
+            return None
+           
+        # not sure this can still happen but we are making sure.
+        # should actually been handled by the DiscoveryFailure exception above
+        if openid_request is None:
+            environ[self.error_field] = 'No OpenID services found for %s' %openid_url
+            environ['repoze.who.logger'].info('No OpenID services found for: %s ' %openid_url)
+            return self._redirect_to_loginform(environ)
+       
+        # we have to tell the openid provider where to send the user after login
+        # so we need to compute this from our path and application URL
+        # we simply use the URL we are at right now (which is the form)
+        # this will be captured by the repoze.who identification plugin later on
+        # it will check if some valid openid response is coming back
+        # trust_root is the URL (realm) which will be presented to the user
+        # in the login process and should be your applications url
+        # TODO: make this configurable?
+        # return_to is the actual URL to be used for returning to this app
+        return_to = request.path_url # we return to this URL here
+        trust_root = request.application_url
+        environ['repoze.who.logger'].debug('setting return_to URL to : %s ' %return_to)
+        
+        # TODO: usually you should check openid_request.shouldSendRedirect()
+        # but this might say you have to use a form redirect and I don't get why
+        # so we do the same as plone.openid and ignore it.
+
+        # TODO: we might also want to give the application some way of adding
+        # extensions to this message.
+        redirect_url = openid_request.redirectURL(trust_root, return_to) 
+        # # , immediate=False)
+        res = Response()
+        res.status = 302
+        res.location = redirect_url
+        environ['repoze.who.logger'].debug('redirecting to : %s ' %redirect_url)
+
+        # now it's redirecting and might come back via the identify() method
+        # from the openid provider once the user logged in there.
+        return res
+        
+    def _redirect_to_loginform(self, environ={}):
+        """redirect the user to the login form"""
+        res = Response()
+        res.status = 302
+        q=''
+        ef = environ.get(self.error_field, None)
+        if ef is not None:
+                q='?%s=%s' %(self.error_field, ef)
+        res.location = self.login_form_url+q
+        return res
+        
+                
+    # IAuthenticator
+    def authenticate(self, environ, identity):
+        """dummy authenticator
+        
+        This takes the openid found and uses it as the userid. Normally you would want
+        to take the openid and search a user for it to map maybe multiple openids to a user.
+        This means for you to simply implement something similar to this. 
+        
+        """
+        if identity.has_key("repoze.who.plugins.openid.userid"):
+                environ['repoze.who.logger'].info('authenticated : %s ' %identity['repoze.who.plugins.openid.userid'])
+                return identity.get('repoze.who.plugins.openid.userid')
+
+
+    def __repr__(self):
+        return '<%s %s>' % (self.__class__.__name__, id(self))
+

repoze/who/plugins/openid/identification.py

+import cgi
+import urlparse
+import cgitb
+import sys
+from zope.interface import implements
+
+from repoze.who.interfaces import IChallenger
+from repoze.who.interfaces import IIdentifier
+from repoze.who.interfaces import IAuthenticator
+
+from webob import Request, Response
+
+import openid
+from openid.store import memstore, filestore, sqlstore
+from openid.consumer import consumer
+from openid.oidutil import appendArgs
+from openid.cryptutil import randomString
+from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
+from openid.extensions import pape, sreg
+
+
+
+class OpenIdIdentificationPlugin(object):
+    """The repoze.who OpenID plugin
+    
+    This class contains 3 plugin types and is thus implementing
+    IIdentifier, IChallenger and IAuthenticator.
+    (check the `repoze.who documentation <http://static.repoze.org/bfgdocs/>`_
+    for what all these plugin types do.)
+    
+    """
+
+
+    implements(IChallenger, IIdentifier, IAuthenticator)
+
+    def __init__(self, store, 
+                    openid_field, 
+                    error_field = '',
+                    store_file_path='',
+                    session_name = '',
+                    login_handler_path = '',
+                    logout_handler_path = '',
+                    login_form_url = '',
+                    logged_in_url = '',
+                    logged_out_url = '',
+                    came_from_field = '',
+                    rememberer_name = '',
+                    sql_associations_table = '',
+                    sql_nonces_table = '',
+                    sql_connstring = ''):
+
+        self.rememberer_name = rememberer_name
+        self.login_handler_path = login_handler_path
+        self.logout_handler_path = logout_handler_path
+        self.login_form_url = login_form_url
+        self.session_name = session_name
+        self.error_field = error_field
+        self.came_from_field = came_from_field
+        self.logged_out_url = logged_out_url
+        self.logged_in_url = logged_in_url
+        
+        # for the SQL store
+        self.sql_associations_table = sql_associations_table
+        self.sql_nonces_table = sql_nonces_table
+        self.sql_connstring = sql_connstring
+        
+        # set up the store
+        if store==u"file":
+            self.store = filestore.FileOpenIDStore(store_file_path)
+        elif store==u"mem":
+            self.store = memstore.MemoryStore()
+        elif store==u"sql":
+            # TODO: This does not work as we need a connection, not a string
+            self.store = sqlstore.SQLStore(sql_connstring, sql_associations_table, sql_connstring)
+        self.openid_field = openid_field
+            
+    def _get_rememberer(self, environ):
+        rememberer = environ['repoze.who.plugins'][self.rememberer_name]
+        return rememberer
+
+    def get_consumer(self,environ):
+        session = environ.get(self.session_name,{})
+        return consumer.Consumer(session,self.store)
+        
+    def redirect_to_logged_in(self, environ):
+        """redirect to came_from or standard page if login was successful"""
+        request = Request(environ)
+        came_from = request.params.get(self.came_from_field,'')
+        if came_from!='':
+            url = came_from
+        else:
+            url = self.logged_in_url
+        res = Response()
+        res.status = 302
+        res.location = url
+        environ['repoze.who.application'] = res    
+
+    # IIdentifier
+    def identify(self, environ):
+        """this method is called when a request is incoming.
+
+        After the challenge has been called we might get here a response
+        from an openid provider.
+
+        """
+
+        request = Request(environ)
+        
+        # first test for logout as we then don't need the rest
+        if request.path == self.logout_handler_path:
+            res = Response()
+            # set forget headers
+            for a,v in self.forget(environ,{}):
+                res.headers.add(a,v)
+            res.status = 302
+            res.location = self.logged_out_url
+            environ['repoze.who.application'] = res
+            return {}
+
+        identity = {}
+
+        # first we check we are actually on the URL which is supposed to be the
+        # url to return to (login_handler_path in configuration)
+        # this URL is used for both: the answer for the login form and
+        # when the openid provider redirects the user back.
+        if request.path == self.login_handler_path:
+
+        # in the case we are coming from the login form we should have 
+        # an openid in here the user entered
+            open_id = request.params.get(self.openid_field, None)
+            environ['repoze.who.logger'].debug('checking openid results for : %s ' %open_id)
+            
+            if open_id is not None:
+                open_id = open_id.strip()
+            
+            # we don't do anything with the openid we found ourselves but we put it in here
+            # to tell the challenge plugin to initiate the challenge
+            identity['repoze.whoplugins.openid.openid'] = environ['repoze.whoplugins.openid.openid'] = open_id
+        
+            # this part now is for the case when the openid provider redirects
+            # the user back. We should find some openid specific fields in the request.
+            mode=request.params.get("openid.mode", None)
+            if mode=="id_res":
+                oidconsumer = self.get_consumer(environ)
+                info = oidconsumer.complete(request.params, request.url)
+
+                if info.status == consumer.SUCCESS:
+                    environ['repoze.who.logger'].info('openid request successful for : %s ' %open_id)
+                    
+                    display_identifier = info.identity_url
+                    
+                    # remove this so that the challenger is not triggered again
+                    del environ['repoze.whoplugins.openid.openid']
+                    
+                    # store the id for the authenticator
+                    identity['repoze.who.plugins.openid.userid'] = display_identifier
+
+                    # now redirect to came_from or the success page
+                    self.redirect_to_logged_in(environ)
+                    return identity
+                    
+                # TODO: Do we have to check for more failures and such?
+                # 
+            elif mode=="cancel":
+                # cancel is a negative assertion in the OpenID protocol,
+                # which means the user did not authorize correctly.
+                environ['repoze.whoplugins.openid.error'] = 'OpenID authentication failed.'
+                pass
+        return identity
+
+    # IIdentifier
+    def remember(self, environ, identity):
+        """remember the openid in the session we have anyway"""
+        rememberer = self._get_rememberer(environ)
+        r = rememberer.remember(environ, identity)
+        return r
+
+    # IIdentifier
+    def forget(self, environ, identity):
+        """forget about the authentication again"""
+        rememberer = self._get_rememberer(environ)
+        return rememberer.forget(environ, identity)
+
+    # IChallenge
+    def challenge(self, environ, status, app_headers, forget_headers):
+        """the challenge method is called when the ``IChallengeDecider``
+        in ``classifiers.py`` returns ``True``. This is the case for either a 
+        ``401`` response from the client or if the key 
+        ``repoze.whoplugins.openid.openidrepoze.whoplugins.openid.openid``
+        is present in the WSGI environment.
+        The name of this key can be adjusted via the ``openid_field`` configuration
+        directive.
+
+        The latter is the case when we are coming from the login page where the
+        user entered the openid to use.
+
+        ``401`` can come back in any case and then we simply redirect to the login
+        form which is configured in the who configuration as ``login_form_url``.
+
+        TODO: make the environment key to check also configurable in the challenge_decider.
+
+        For the OpenID flow check `the OpenID library documentation 
+        <http://openidenabled.com/files/python-openid/docs/2.2.1/openid.consumer.consumer-module.html>`_
+
+        """
+
+        request = Request(environ)
+        
+        # check for the field present, if not redirect to login_form
+        if not request.params.has_key(self.openid_field):
+            # redirect to login_form
+            res = Response()
+            res.status = 302
+            res.location = self.login_form_url+"?%s=%s" %(self.came_from_field, request.url)
+            return res
+
+        # now we have an openid from the user in the request 
+        openid_url = request.params[self.openid_field]
+        environ['repoze.who.logger'].debug('starting openid request for : %s ' %openid_url)       
+
+        try:
+        # we create a new Consumer and start the discovery process for the URL given
+        # in the library openid_request is called auth_req btw.
+            openid_request = self.get_consumer(environ).begin(openid_url)
+        except consumer.DiscoveryFailure, exc:
+            # eventually no openid server could be found
+            environ[self.error_field] = 'Error in discovery: %s' %exc[0]
+            environ['repoze.who.logger'].info('Error in discovery: %s ' %exc[0])     
+            return self._redirect_to_loginform(environ)
+        except KeyError, exc:
+            # TODO: when does that happen, why does plone.openid use "pass" here?
+            environ[self.error_field] = 'Error in discovery: %s' %exc[0]
+            environ['repoze.who.logger'].info('Error in discovery: %s ' %exc[0])
+            return self._redirect_to_loginform(environ)
+            return None
+           
+        # not sure this can still happen but we are making sure.
+        # should actually been handled by the DiscoveryFailure exception above
+        if openid_request is None:
+            environ[self.error_field] = 'No OpenID services found for %s' %openid_url
+            environ['repoze.who.logger'].info('No OpenID services found for: %s ' %openid_url)
+            return self._redirect_to_loginform(environ)
+       
+        # we have to tell the openid provider where to send the user after login
+        # so we need to compute this from our path and application URL
+        # we simply use the URL we are at right now (which is the form)
+        # this will be captured by the repoze.who identification plugin later on
+        # it will check if some valid openid response is coming back
+        # trust_root is the URL (realm) which will be presented to the user
+        # in the login process and should be your applications url
+        # TODO: make this configurable?
+        # return_to is the actual URL to be used for returning to this app
+        return_to = request.path_url # we return to this URL here
+        trust_root = request.application_url
+        environ['repoze.who.logger'].debug('setting return_to URL to : %s ' %return_to)
+